|
Download docs/prompt-compatibility.md from a3216/ds2api: direct link, hf CLI and curl.
- Browser
- Download file 35.7 kB
-
https://huggingface.co/spaces/a3216/ds2api/resolve/main/docs/prompt-compatibility.md
- Command line
-
hf download hf://spaces/a3216/ds2api/docs/prompt-compatibility.md
-
curl -L -o prompt-compatibility.md https://huggingface.co/spaces/a3216/ds2api/resolve/main/docs/prompt-compatibility.md
35.7 kB
| # API → 纯文本 web-chat 上下文兼容流程 | |
| 本文是本仓库「把结构化 API 请求翻译成 DeepSeek web-chat 纯文本 prompt,再把上游回答翻译回目标协议」这条链路的源文档。AGENTS.md 规定:任何影响消息归一化、工具提示词注入、提示词可见的工具历史、文件/引用处理、历史拆分、完成载荷组装的改动,都要在同一变更里更新本文。 | |
| 代码位置对应关系: | |
| | 环节 | 主要代码 | | |
| | --- | --- | | |
| | 协议适配器 | `internal/httpapi/openai/`、`internal/httpapi/claude/`、`internal/httpapi/gemini/`、`internal/httpapi/ollama/` | | |
| | 标准化与 prompt 拼装 | `internal/promptcompat/`、`internal/prompt/` | | |
| | 思考/联网解析 | `internal/util/thinking.go`、`internal/util/search.go`、`internal/config/models.go` | | |
| | 上下文拆分 | `internal/httpapi/openai/history/current_input_file.go` | | |
| | 工具调用开关 / 技能注入 | `internal/promptcompat/`(提示词拼装)、Admin `/admin/skills` 处理器 | | |
| | 完成载荷 | `internal/promptcompat/standard_request.go`(`CompletionPayload`) | | |
| | 响应归档 | `internal/chathistory/`、`internal/httpapi/admin/history/` | | |
| --- | |
| ## 1. 端到端流程 | |
| ```text | |
| 协议适配器(OpenAI Chat / OpenAI Responses / Claude / Gemini / Ollama) | |
| │ 只做:解析协议请求 → 归一化消息 → 填 StandardRequest → 回写协议响应 | |
| ▼ | |
| StandardRequest(messages、final_prompt、thinking、search、tool 策略、ref_file_ids …) | |
| │ | |
| ├─ 执行契约注入(thinking_injection,旧称「思考格式注入」) | |
| │ 写入最新一条 user 消息,并落到 final_prompt 的助手起笔标记之前 | |
| ▼ | |
| 上下文衔接(current input file / 历史拆分) | |
| │ 达到阈值时上传「对话记录」文件,改写 prompt 只留衔接指令(内含「执行要求」) | |
| │ tools.enabled=false 时不注入工具说明(也不上传工具说明文件),不解析 tool_calls | |
| │ 只有本轮真的发生了拆分,才把执行契约重新贴回可见 prompt(见第 4 节) | |
| ▼ | |
| prompt 级叠加(模型提示词 / 特化层 → 技能层) | |
| │ 自定义模型带提示词时用它,否则用 prompt_profile(两者互斥,都缺省关闭) | |
| │ skills(缺省关闭)在内层;全部合并成一个 System 段前置到 prompt 最前面 | |
| │ 只改 prompt / PromptTokenText,不改 messages | |
| ▼ | |
| 组装 DeepSeek completion payload | |
| │ thinking_enabled / search_enabled / model_type / prompt / ref_file_ids | |
| ▼ | |
| 上游 SSE → 协议回写(含空输出重试、工具调用解析、引用标记处理)→ 响应记录 | |
| ``` | |
| | 阶段 | 输入 | 输出 | 共享性 | | |
| | --- | --- | --- | --- | | |
| | 协议归一化 | HTTP 请求体 | `StandardRequest` | 每个适配器一份,属边界代码 | | |
| | 模型/思考/联网解析 | 请求字段 + 模型名 | `Thinking`、`Search`、`ModelType` | 全协议共享,只有一处 | | |
| | 执行契约注入 | `StandardRequest` + API Key 注入设置 | 注入后的消息与 prompt | 全协议共享 | | |
| | 历史拆分 | 注入后的消息 | 上下文文件 + 精简 prompt + 重贴的契约 | 全协议共享 | | |
| | prompt 级叠加 | 拆分后的 prompt | 前置了 System 段(特化层 + 技能)的 prompt | 全协议共享,只改 prompt 词串 | | |
| | 载荷组装 | `StandardRequest` | `map[string]any` payload | 全协议共享 | | |
| | 响应回写 | 上游 SSE / JSON | 协议响应 + 记录 | 适配器渲染 + 共享归档 | | |
| 四个会产生对话的入口(`/v1/chat/completions`、`/v1/responses`、`/v1/messages`、Gemini `generateContent`)都调用同一个 `history.Service.ApplyCurrentInputFile`,没有第二套历史拆分实现。Ollama 只提供 `/api/version`、`/api/tags`、`/api/show`,不参与对话链路。 | |
| --- | |
| ## 2. 统一标准请求 | |
| `promptcompat.StandardRequest` 是跨协议唯一的数据形状。适配器填它,共享逻辑读它,渲染层再据此回写。 | |
| | 字段 | 含义 | | |
| | --- | --- | | |
| | `Surface` | 来源接口,如 `openai_chat`、`openai_responses`、`anthropic_messages`、`google_gemini` | | |
| | `RequestedModel` / `ResolvedModel` / `ResponseModel` | 客户端原始模型名 / 解析后的规范模型 / 响应里回显的模型名 | | |
| | `Messages` | 归一化后的消息数组,历史拆分与注入都作用于它 | | |
| | `FinalPrompt` | 拼装完成的纯文本 prompt | | |
| | `PromptTokenText` | 用于 token 估算的文本,通常与 `FinalPrompt` 同步改写 | | |
| | `ToolsRaw` / `ToolNames` / `ToolChoice` | 工具定义、可用工具名、`auto`/`none`/`required`/`forced` 策略 | | |
| | `Thinking` / `Search` | 本次请求实际生效的思考、联网开关 | | |
| | `RefFileIDs` / `FileInfos` | 送上上游的文件引用 ID 与归档用的文件元信息 | | |
| | `CurrentInputFileApplied` / `CurrentInputFileID` / `CurrentInputFilename` / `CurrentToolsFileID` | 历史拆分产生的状态,用于切号重传时替换旧文件 | | |
| | `PassThrough` | 需要透传的采样参数(`temperature`、`top_p`、`max_tokens` 等) | | |
| --- | |
| ## 3. 纯文本 web-chat 上下文约定 | |
| DeepSeek web-chat 只接受一段 prompt 字符串和一组文件引用,没有 role 数组。角色信息只能用文本标记表达: | |
| | 标记 | 对应角色 | 说明 | | |
| | --- | --- | --- | | |
| | `System: ` | system / developer | `developer` 归一化为 `system` | | |
| | `User: ` | user | 用户轮次 | | |
| | `Assistant: ` | assistant | 助手轮次;同时也是 prompt 末尾的起笔标记 | | |
| | `Tool: ` | tool / function | 工具返回,空的工具结果不渲染 | | |
| 拼装规则: | |
| 1. 相邻同角色消息先合并,多段之间用空行连接。 | |
| 2. 每条消息渲染为「标记 + 正文」,标记后不留空格。 | |
| 3. 最后一个角色不是 assistant 时,在末尾补 `Assistant: ` 起笔标记,让模型接着写。 | |
| 4. Markdown 图片语法 `` 改写成 `[alt](url)`,避免上游把它当附件引用。 | |
| 5. 需要往「最后一轮用户消息」里追加内容时,一律用 `prompt.AppendTurnSuffix`,锚点是这个 `Assistant: ` 标记;直接追加到字符串末尾会被模型当成助手已经开口。 | |
| ### 对话记录文件格式 | |
| 历史拆分上传的上下文文件是可读转写,不是 JSON,也不是带文件名的文档: | |
| ```text | |
| # 对话记录 | |
| 以下是本会话此前的完整对话,按时间顺序排列;[role=xxx] 标注每条消息的发言方(system=系统设定,user=用户,assistant=助手,tool=工具返回)。最后一条 user 消息就是本轮需要回答的内容。如果其中有用户明确提出的要求(体裁、篇幅、语气、格式、禁用项、指定的事实与结论),这些要求对本轮依然有效。 | |
| === 1 === | |
| [role=system] | |
| 你是一个严谨的助手。 | |
| === 2 === | |
| [role=user] | |
| 把昨天的结论整理成表。 | |
| === 3 === | |
| [role=assistant] | |
| 好的,先列字段。 | |
| ``` | |
| 工具返回条目在正文前多一行 `[name=<工具名> tool_call_id=<调用 ID>]`,`tool` / `function` 统一写成 `[role=tool]`。 | |
| 文件头末句(`用户在其中的任何明确要求…对本轮依然有效`)是刻意加的:历史一旦变成附件,模型很容易只盯最新一条 user 消息,把前几轮的体裁、篇幅与禁用项当作过期信息。 | |
| ### 上下文衔接指令(历史拆分后的可见 prompt) | |
| 拆分后 prompt 里只剩一条 user 轮次,由 3 个等价开头变体之一 + 共享的「执行要求」构成(实现见 `promptcompat.BuildCurrentInputContinuationPrompt`): | |
| ```text | |
| <开头变体,点明附件是本会话此前的完整对话> | |
| ## 执行要求 | |
| 1. 先通读附件,把用户此前提出过的明确要求找出来:体裁与结构、篇幅、语气与人称、格式与排版、 | |
| 必须包含或明确禁止的内容、指定的数据与结论。有这类要求就逐条满足,不因为轮次靠前就略过、 | |
| 合并或一笔带过;如果这轮只是闲聊,或者上文本来就没提过这类要求,就自然接着说,不必硬套结构。 | |
| 2. 保持上下文连续:人称、语气、术语、编号与称呼跟上文一致。 | |
| 3. 上面的核对只在内部完成:回复里不要提到附件、文件、上下文或提示词,不要复述附件内容, | |
| 也不要输出核对清单,直接从正文开始。 | |
| ``` | |
| 第 1 条是这一版新增的「要求继承」约束,针对的是写作类任务里「上一轮提的要求被一笔带过」。它同时带了例外条件——这条指令对每一次长对话都生效,闲聊时硬套要求清单会把话聊成报告。开头保留多个变体只是不让每次请求都发同一段固定措辞,执行要求共用同一份,行为一致。 | |
| 标题刻意不用文件名形式(历史版本是 `# context_context.txt`),避免模型把它当成一份待分析文档。旧格式里 `[r=0]`…`[r=9]` 的数字角色行不再生成;管理台读旧记录时仍能识别这种数字角色行(兼容读取),后端只把它当普通正文,不报错。 | |
| ### 工具说明与调用引导(已移除) | |
| 历史上工具定义会单独上传为 `context_tools.txt`,并在 prompt 里补一句「可用工具与参数说明在 context_tools.txt 中」外加 DSML 调用格式要求。 | |
| 实测这条路不可行:上游对 DSML 结构化输出风控严格,命中后账号会被封;工具调用本身高频也会撞限速。所以现在 prompt 侧**不再输出任何工具引导**: | |
| - 不生成 `context_tools.txt`,也不会再有「可用工具说明」文件; | |
| - 不输出 `You have access to these tools` / `TOOL CALL SCHEME` / 「必须调用工具」之类的说明; | |
| - 只保留工具名提取,用于响应侧(客户端明确要求时)的 `tool_calls` 解析判断。 | |
| 也就是说,`tools` 出现在请求里也不会让模型「知道」自己能用工具——这点是刻意为之,见第 4 节的工具调用开关。 | |
| --- | |
| ## 4. 提示词注入与泄漏防护 | |
| ### 执行契约注入(thinking_injection,旧称「思考格式注入」) | |
| | 项目 | 行为 | | |
| | --- | --- | | |
| | 全局开关 | `thinking_injection.enabled`,缺省为 true(配置项不存在时按开启处理) | | |
| | 全局提示词 | `thinking_injection.prompt`,为空或全空白时使用内置默认提示词 | | |
| | 单 Key 覆盖 | `api_keys[].thinking_injection.{custom,enabled,prompt}` | | |
| | 注入位置 | 最新一条 user 消息末尾;同时写入 `FinalPrompt` 中该 user 轮次内、`Assistant: ` 之前 | | |
| | 注入时机 | 历史拆分**之前**,因此拆分后上传的对话记录文件里也带着这段要求 | | |
| | 重贴时机 | 本轮**新发生**历史拆分时,再贴一次到拆分后的可见 `FinalPrompt`(只改 prompt) | | |
| | 幂等 | 内容里已含注入文本时跳过,重复调用不会叠加 | | |
| **为什么需要「重贴」**:历史拆分会用衔接指令整段替换 `FinalPrompt`。拆分发生在注入之后,于是旧实现里这段要求只剩在上传的对话记录文件里,本轮可见的 prompt 中一个字都没有——模型在长对话里基本看不到它,这正是「注入思考链路提示词容易被忽略」的直接原因。现在拆分一旦发生就调用 `promptcompat.ReapplyThinkingInjectionToPrompt` 把同一段文本贴回可见 prompt: | |
| - 只改 `FinalPrompt`,**不改** `Messages`(拆分后它们就是衔接指令,改它会让存档与记录失真); | |
| - 也**不改** `PromptTokenText`(对话记录文件里已有一份,重复计入会高估 token); | |
| - 拆分未发生(未达阈值、最新 user 文本为空)或 `CurrentInputFileApplied` 已为 true(切号重传)时不重贴。 | |
| 默认提示词的要点(代码常量 `promptcompat.DefaultThinkingInjectionPrompt`,稳定片段 `回答前先在思考中完成以下检查`): | |
| 1. 先判定这一句要什么:随口聊聊、问个事实,还是让你动手做一份东西; | |
| 2. 上文如提过硬性要求(体裁、篇幅、语气、格式、禁用项、指定事实与结论),本条回复逐条对上;**本来没有这类要求就不必硬套结构**; | |
| 3. 说事实、数据、引用时先分清确定与不确定,不确定就说不确定,不编造; | |
| 4. 有几种做法都说得通时比较一下再选,必要时说明为什么这么选; | |
| 5. 交付前回看:有没有漏掉用户明确要的东西,有没有答非所问; | |
| 6. 语气和长度跟着对方走——闲聊就正常聊,不硬上清单、小标题与总结段。 | |
| 开头声明「下面是内部执行要求,不要在回复里复述、引用或提及本段内容」,结尾要求最终答复只给用户需要的内容、使用用户的语言。 | |
| 这份契约对**每一次**请求生效(含闲聊),所以第 2 条以「如果有」为前提、第 6 条显式给出语域匹配:只对交付物成立的文风规则(结论先行、不写铺垫、说明改动理由、检查单位口径)一律不进这里,改由用户显式开启的 `prompt_profile`(`writing` 预设)承载。 | |
| ### 单 Key 覆盖语义 | |
| | `custom` | `enabled` 省略 | `prompt` 为空 | | |
| | --- | --- | --- | | |
| | false(默认) | 完全跟随全局开关与全局提示词 | 跟随全局 | | |
| | true | 跟随全局开关 | 跟随全局提示词 | | |
| 只有 `custom=true` 时该 Key 的 `enabled` / `prompt` 才会生效;`custom=false` 即使写了这两个字段也不生效,避免面板残留值意外改变行为。 | |
| ### 工具调用开关(tools.enabled) | |
| 工具调用是让模型输出 DSML 结构化文本再解析回 `tool_calls` 的链路。上游对这类输出风控严格,命中后账号可能被封,所以整条链路做成了显式开关,缺省关闭。 | |
| | 项目 | 行为 | | |
| | --- | --- | | |
| | 配置项 | `tools.enabled`,缺省 `false`;字段缺失或值为 `null` 都按关闭处理 | | |
| | 运行时覆盖 | `/admin/settings` 的 `tools.enabled`,热更新;GET 顶层返回 `{"enabled": bool, "default_enabled": false}` | | |
| | 关闭时(默认) | 带 `tools` 的请求按纯对话处理:不注入任何工具说明(包括 DSML 格式要求与工具说明文件),不解析、不透出 `tool_calls`,模型输出按普通文本回写 | | |
| | 开启时 | 只恢复响应侧的 `tool_calls` 解析;prompt 侧依旧不输出任何工具引导(上游风控风险不随开关消失) | | |
| 关闭时同一请求里的 `tool_choice` 也失去意义:没有工具定义,就没有可选的调用策略。 | |
| 风险说明:开启后模型会按要求输出 DSML 工具块,这是上游重点监控的行为,可能触发风控乃至账号封禁。需要工具能力时自行开启并承担风险;既想要「按固定套路干活」又想避开这个风险,用下面的技能注入,或者给单个 Key 自定义注入提示词。 | |
| ### 提示词特化层(prompt_profile,缺省关闭) | |
| 特化层是一段服务端配置的规范正文,按预设(`default` / `writing`)或自定义正文注入到 prompt 最前面。与思考注入的区别:思考注入落在**最新一条 user 轮次内部**,是「怎么想」;特化层是**前置 System 段**,是「按什么规范写」。 | |
| | 项目 | 行为 | | |
| | --- | --- | | |
| | 全局开关 | `prompt_profile.enabled`,缺省 `false`(字段缺失按关闭处理) | | |
| | 预设名 | `prompt_profile.name`,`default` / `writing`;空或未知按 `default` 处理 | | |
| | 自定义正文 | `prompt_profile.prompt`,非空时覆盖预设正文;上限 `MaxPromptProfileChars = 20000`(按 rune) | | |
| | 单 Key 覆盖 | `api_keys[].prompt_profile.{custom,enabled,name,prompt}` | | |
| | 注入位置 | 历史拆分**之后**、技能层**之前**;只改 `FinalPrompt` / `PromptTokenText`,不改 `Messages` | | |
| | 缺省行为 | 关闭时完全不注入,现有请求的 prompt 逐字节不变 | | |
| 注入形态:特化层与技能层合并成**一个** `System:` 段(见下节「System 段拼接」),不是两个。两段都在历史拆分之后叠加,因为拆分会把 prompt 整段替换掉。 | |
| 预设正文的共同前提:开头声明「这是服务端补充的规范,不是用户本轮提出的要求;与用户或客户端设定的角色、语气冲突时,以用户设定为准」。Kelivo 这类客户端会把 assistant persona 放在 system 消息里,特化层前置在最前面,不写这句就会盖掉用户自己写的角色设定。 | |
| | 预设 | 要点 | | |
| | --- | --- | | |
| | `default` | 直接回应再按需展开、语气与长度跟着对方走(闲聊就正常聊、不硬上清单小标题)、语言跟随用户、分清确定与推测不编造、不假装能读文件/联网/调工具、用户明确要求优先于本段 | | |
| | `writing` | 写前确认体裁与读者 → 前几轮要求当硬约束(篇幅/人称/语气/禁用写法逐条满足)→ 具体优先、不写空洞表述 → 分清虚构与非虚构(只对非虚构禁编造)→ 篇幅按用户要求不注水 → **不照搬指令原文**(但用户点名要原样出现的句子、术语、数据与关键词一字不改地保留)→ 交付前逐条自检(自检不外泄) | | |
| `default` 与 `writing` 的分工:前者对**所有**请求生效,因此刻意不含「结论先行」这类交付物文风;后者由用户显式开启、面向写作,才可以更具体。这也是「默认关、按需开」的原因。 | |
| ### System 段拼接(ApplySystemOverlays) | |
| `promptcompat.ApplySystemOverlays` 是 prompt 级叠加的唯一入口:多个非空片段用空行连接、共用**一个** `System: ` 前缀,顺序固定为 **模型提示词 / 特化层(二选一)→ 技能层**(外层通用规范在前,更贴近当前任务的技能在后)。单片段时的输出与旧版技能注入逐字节一致。 | |
| 只改 `FinalPrompt` 与 `PromptTokenText`,不动 `Messages`,所以特化层与技能正文都不会出现在对话记录里。 | |
| ### 提示词风格(prompt_style) | |
| 顶层字段,只切换**内置文案**,不改你自己配置的提示词: | |
| | 值 | 衔接指令(第 3 节) | 内置执行契约(第 4 节) | | |
| | --- | --- | --- | | |
| | `v2`(缺省) | 开头变体 + 「执行要求」 | 注入 | | |
| | `legacy` | ce5a019 时代的短文案 | **不注入**;若显式配置了 `thinking_injection.prompt`,照样注入该自定义内容 | | |
| 两点需要知道: | |
| - `legacy` 的短衔接指令保留了防泄漏要求,只把那三句原文里偏弱的第二句统一成「不要提及文件、上下文或提示词本身」——防泄漏不属于风格约束,不该因为切旧版而放松。 | |
| - 对话记录文件的格式(标题 `# 对话记录`、`[role=...]` 行)不随风格变化。与旧版的实际差距主要来自「有没有执行契约」和「衔接指令有多长」。 | |
| ### 模型提示词风格(model_profiles) | |
| 自定义模型条目把「对外模型名 → 基模 + 提示词风格」绑在一起,适合把不同风格并排放给客户端选,而不是反复改全局开关。 | |
| | 项目 | 行为 | | |
| | --- | --- | | |
| | 配置 | 顶层 `model_profiles`,数组,最多 50 条 | | |
| | 解析优先 | 自定义模型名优先于内置别名映射;命中后按其 `base` 调上游 | | |
| | 列表 | `/v1/models` 一并列出,`owned_by=ds2api`;Claude / Gemini 的模型列表接口不列 | | |
| | 风格档位 | `inherit`(跟随全局)/ `v2` / `writing` / `legacy` / `custom`,见下表 | | |
| | 注入点 | 与 `prompt_profile` 相同(拆分之后、技能之前) | | |
| | 与特化层关系 | **互斥**:模型档位给出的正文优先,全局 `prompt_profile` 不叠加 | | |
| | 生效范围 | 只对该模型名生效;客户端换回 `deepseek-v4-flash` 就等于关掉它 | | |
| | 管理接口 | `GET/PUT /admin/model_profiles`,整体替换语义 | | |
| 一个条目同时决定「内置文案用哪版」与「注不注入特化层」: | |
| | `style` | 衔接指令 / 执行契约 | 特化层正文 | | |
| | --- | --- | --- | | |
| | `inherit` | 全局 `prompt_style` | 全局 `prompt_profile` | | |
| | `v2` | 固定 v2 | 不注入 | | |
| | `writing` | 固定 v2 | 内置写作规范正文 | | |
| | `legacy` | 固定旧版短文案、不注入执行契约 | 不注入 | | |
| | `custom` | 全局 `prompt_style` | 条目自己的 `prompt` | | |
| 兼容说明:早期条目只有 `prompt` 字段、没有 `style`,这种配置按 `custom` 解释(`EffectiveStyle`),升级后行为不变。 | |
| ### 技能注入(skills) | |
| 技能是一段服务端配置的纯文本 markdown,直接拼进 prompt。模型全程不需要输出工具调用,看到的只是普通上下文,因此不会碰到 DSML 风控。 | |
| | 项目 | 行为 | | |
| | --- | --- | | |
| | 全局开关 | `skills.enabled`,缺省 `false`(实验性,默认不注入) | | |
| | 截断长度 | `max_chars`,缺省 `8000`,按字符(rune)统计;截断时在末尾追加「(技能内容已截断)」;`0` 表示不限制 | | |
| | 条目数 | `items` 上限 50 条 | | |
| | `id` | 必填、唯一,字符集 `[a-z0-9_-]` | | |
| | `body` | 必填,单条上限 100000 字符,全部条目合计上限 500000 字符 | | |
| | 管理接口 | `GET/PUT /admin/skills`,需要 Admin 鉴权,PUT 为整体替换 | | |
| | 校验失败 | 超限、重复 `id`、非法 `id` 一律 400 | | |
| 单 Key 覆盖(`api_keys[].skills = {"custom": ..., "enabled"?: ..., "ids"?: [...]}`): | |
| | `custom` | `enabled` | `ids` | 结果 | | |
| | --- | --- | --- | --- | | |
| | 省略或 `false` | 任意 | 任意 | 使用全部 `items` | | |
| | `true` | `false` | 任意 | 该 Key 不注入 | | |
| | `true` | 省略或 `true` | 非空 | 只用命中的 `items` | | |
| | `true` | 省略或 `true` | 空 | 不注入 | | |
| 全局 `skills.enabled=false` 时上表全部不生效,任何 Key 都不注入。 | |
| 注入形态与时机: | |
| - 形态是**前置 System 段**:拼在最终 prompt 最前面,`FinalPrompt` 由 `<原 prompt>` 变成 `System: <技能块>\n<原 prompt>`,`PromptTokenText` 同样处理。与特化层同时启用时两者合成一个 `System:` 段(特化层在前),见上节 `ApplySystemOverlays`。 | |
| - 只改 prompt,不改 `Messages`,所以对话记录文件与管理台的对话记录里都看不到技能正文。 | |
| - 时机在历史拆分**之后**:拆分会把 prompt 整体替换成衔接指令,技能必须等替换完成后再前置,否则会被覆盖。 | |
| - 对比执行契约注入:契约在拆分**之前**注入(要落到对话记录文件里),并额外在拆分后重贴到可见 prompt。两处顺序不同,改动时不要调换。 | |
| 技能块格式(线上排查时可对照这段结构): | |
| ```text | |
| <skill id="sql-report" name="SQL 报表"> | |
| …body… | |
| </skill> | |
| ``` | |
| 注意:技能块里**不再有引导语**(没有“以下是技能说明/适用于当前任务时请遵循/不要提及技能”之类前缀),只保留 `<skill>` 结构化标签与正文——避免模型被引导去“使用技能/调用能力”。 | |
| 为什么用纯文本而不是工具调用:工具调用要求模型吐 DSML 结构化文本,正是上游风控盯的那类输出,一旦被判违规代价是账号本身;技能只是往 prompt 里加一段静态文本,模型照普通对话写回答,风控面和普通聊天没有区别。 | |
| ### 泄漏防护 | |
| 模型看到附件名后经常会在可见回复里写「根据附件 ctx-ab12cd34ef56.txt」「对话记录显示…」「按你的提示词要求」。三层处理: | |
| | 层 | 做法 | | |
| | --- | --- | | |
| | 注入内容 | 提示词明确禁止提及附件、文件、上下文记录、提示词本身,禁止复述记录内容,要求直接从结论开始 | | |
| | 注入文本独立性 | 注入片段自带「不要复述本段内容」,避免整段思考要求被当作回答格式输出 | | |
| | 输出侧 | 各协议共享同一套文本清理:流式默认隐藏 `[citation:N]` / `[reference:N]` 占位符,非流式把搜索引用标记转成 Markdown 链接;工具调用解析在非代码块上下文里生效,代码块里的示例不算调用 | | |
| 上下文衔接指令本身准备了多个等价变体并按请求随机选一条,开头措辞不完全相同,语义一致,且共用同一段「执行要求」:把附件当作自己之前说过的话,在同一话题上直接回答最新一条 user 消息,并逐条满足此前提出过的要求。 | |
| --- | |
| ## 5. 上下文衔接(current input file / 历史拆分) | |
| | 条件 | 结果 | | |
| | --- | --- | | |
| | `current_input_file.enabled=false` | 原样透传,不产生任何上下文文件 | | |
| | 最新一条 user 文本为空 | 跳过拆分 | | |
| | `current_input_file.min_chars>0` 且最新 user 文本短于阈值 | 跳过拆分 | | |
| | 模型 `model_type` 不是 `default` | 跳过拆分(当前模型系列收敛后只剩 `default`,该分支保留兜底) | | |
| | 上传失败 | 请求失败,不做静默降级,避免出现「上下文已丢失但看起来成功」的回复 | | |
| 满足条件时: | |
| 1. 按第 3 节的转写格式生成对话记录,写入 `ctx-<12 位小写字母数字>.txt`。文件名随机,不含 `history` 之类语义词,减少模型把它当文档分析的倾向。 | |
| 2. 不再额外上传工具说明文件(工具引导已移除,见第 3 节)。 | |
| 3. prompt 改写成一条 user 衔接指令(含文件名与「执行要求」)。原始长对话不再进 prompt。 | |
| 4. 把执行契约重贴到改写后的 `FinalPrompt`(只在 `Assistant: ` 之前插入文本,见第 4 节)。 | |
| 5. 叠加 prompt 级增强层:提示词特化层 → 技能层,合成一个 `System:` 段(两者都缺省关闭)。 | |
| 6. 文件 ID 前置插入 `RefFileIDs`,与客户端自带的 `ref_file_ids` 去重后一起送上游。 | |
| 7. `PromptTokenText` 保留「对话记录 + 工具说明」全文,用于 token 估算,与实际 prompt 长度解耦。 | |
| 切号重试时(空输出 429 换账号)会调用 `ReuploadAppliedCurrentInputFile`:用同一份 `HistoryText` 重新上传,并把旧的历史文件 ID、工具文件 ID 从 `RefFileIDs` 里替换掉,避免新账号引用到别的账号名下的文件。 | |
| 上游流式生成期间若再次需要拆分(例如重试),沿用已记录的 `CurrentInputFilename`,同一轮对话不会出现两个名字不同的上下文文件。 | |
| --- | |
| ## 6. 模型 / 思考 / 联网解析优先级 | |
| ### 模型 | |
| `/v1/models`、`/api/tags`(Ollama)、`/anthropic/v1/models` 只公布 flash 系列: | |
| | 对外模型 ID | 默认思考 | 默认联网 | | |
| | --- | --- | --- | | |
| | `deepseek-v4-flash` | 开 | 关 | | |
| | `deepseek-v4-flash-search` | 开 | 开 | | |
| | `deepseek-v4-flash-nothinking` | 强制关 | 关 | | |
| | `deepseek-v4-flash-search-nothinking` | 强制关 | 开 | | |
| 上游已把 v4-pro / v4-vision 合并进 v4-flash,对外不再公布: | |
| | 旧模型名 | 解析结果 | | |
| | --- | --- | | |
| | `deepseek-v4-pro` | `deepseek-v4-flash` | | |
| | `deepseek-v4-pro-search` | `deepseek-v4-flash-search` | | |
| | `deepseek-v4-vision` | `deepseek-v4-flash` | | |
| 静默映射,保留 `-nothinking` 后缀:`deepseek-v4-pro-nothinking` → `deepseek-v4-flash-nothinking`。老客户端保存过的模型名继续可用,只是不再出现在列表里。 | |
| 上游 `model_type` 只剩 `default`。内置别名(`gpt-*`、`o*`、`claude-*`、`gemini-*`)全部指向 flash 系列,其中 `o3-deep-research`、`o4-mini-deep-research` 指向 `deepseek-v4-flash-search`。`config.model_aliases` 可覆盖或补充同名映射。 | |
| ### 思考 | |
| 优先级从低到高: | |
| 1. 模型默认值(`-nothinking` 变体默认关,其余 flash 模型默认开); | |
| 2. 客户端显式覆盖; | |
| 3. `-nothinking` 模型强制关闭(最高)。 | |
| 客户端覆盖按以下顺序查找,命中即返回,`extra_body` 里的同名键也会被读取: | |
| | 来源 | 接受写法 | | |
| | --- | --- | | |
| | `thinking` | bool;`"enabled"` / `"disabled"`(含 `on/off/true/false/none`);`{"type": ...}` | | |
| | `reasoning` | `{"effort": ...}`、`{"type": ...}`、`{"enabled": ...}`;字符串按 effort 解析 | | |
| | `reasoning_effort` | `minimal` / `low` / `medium` / `high` / `xhigh` / `max` = 开启;`none` / `disabled` / `disable` / `off` / `false` = 关闭 | | |
| Kelivo 这类客户端把界面上的「思维强度」翻译成 `reasoning_effort`:没有这个字段表示跟随默认(即开启思考),传 `none` 表示关闭。`minimal` 到 `max` 都只表示「要思考」,本项目不区分强度档位——上游只有开/关两态。 | |
| ### 联网搜索 | |
| 优先级:模型默认(`-search` 变体为开)→ 客户端显式信号优先。 | |
| 以下任一命中即视为客户端显式开启:`enable_search`(bool 或 `"true"`/`"false"`)、`web_search_options` 出现、`search_options` 出现、`tools` 里带原生搜索类型(`type` 以 `web_search` 开头、`type=google_search`、或工具对象里有 `googleSearch` / `google_search` / `web_search` 字段)。`enable_search` 显式 `false` 也会生效,用于在 `-search` 模型上临时关掉联网。 | |
| 普通 function 工具不算信号。像 Kelivo 客户端自己执行的 `search_web` 始终是普通 function tool,默认仍不开启上游联网。 | |
| 现状说明:Kelivo 对「自定义 OpenAI 兼容服务」不会发送原生搜索标记,所以要用联网请选 `-search` 模型;一旦客户端开始发送上述显式信号,服务端会自动跟随,不需要改配置。 | |
| ### 自定义模型(model_profiles) | |
| `model_profiles` 里的模型名与内置模型走同一套解析:`config.ResolveModel` 先查自定义条目,再退回内置模型与别名映射。因此: | |
| - `-nothinking` / `-search` 这类后缀语义仍然只作用于**基模**:`base` 直接写完整名字(如 `deepseek-v4-flash-search-nothinking`),自定义模型名本身不再二次解释后缀。 | |
| - 思考与联网的默认值同样按解析后的基模决定,因此「某个自定义模型强制关思考」的做法是给它选 `-nothinking` 基模,而不是在条目上加开关。 | |
| - `/v1/models` 会列出全部内置模型 + 自定义模型;`/anthropic/v1/models` 与 Ollama `/api/tags` 仍只列内置模型。 | |
| --- | |
| ## 7. 协议适配器边界 | |
| 适配器只允许做这些事: | |
| | 允许 | 说明 | | |
| | --- | --- | | |
| | 解析协议请求体 | OpenAI、Anthropic、Gemini 各自的字段形状差异 | | |
| | 消息归一化 | 把 `content` 数组、图片块、`tool_use` / `tool_result`、`functionDeclarations` 等翻译成统一消息 | | |
| | 填 `StandardRequest` | 含 `Surface`、模型名、工具、透传参数 | | |
| | 渲染协议响应 | 非流式结构、SSE 事件名与顺序、`finish_reason` / `stop_reason` / `finishReason` 映射 | | |
| | 协议专属错误形态 | 错误对象字段名、HTTP 状态码对齐 | | |
| 不允许在适配器里实现(必须放共享路径): | |
| | 不允许 | 共享实现位置 | | |
| | --- | --- | | |
| | 空输出重试与切号 | `internal/completionruntime/`、各协议的 `empty_retry_runtime` 共用同一策略 | | |
| | 思考/联网解析 | `internal/util/thinking.go`、`internal/util/search.go` | | |
| | 工具调用检测与策略 | `internal/toolcall/`、`internal/httpapi/openai/shared/` | | |
| | 思考格式注入 | `promptcompat.ApplyThinkingInjectionToRequest`(拆分后重贴用 `ReapplyThinkingInjectionToPrompt`) | | |
| | prompt 级叠加(特化层 / 技能) | `promptcompat.ApplySystemOverlays` | | |
| | 历史拆分与文件上传 | `internal/httpapi/openai/history/` | | |
| | 完成载荷组装 | `StandardRequest.CompletionPayload` | | |
| | 响应记录 | `internal/chathistory/` | | |
| 确有必须按协议不同的行为(例如 Claude 输出 `thinking` block 而 Gemini 输出 `parts[].thought=true`),保持在渲染层,并在代码注释里写明为什么不能进共享路径。 | |
| --- | |
| ## 8. 变更记录与兼容性 | |
| | 变更 | 兼容策略 | | |
| | --- | --- | | |
| | 模型系列收敛到 flash 4 个 ID | pro / vision 老名字静默映射到 flash 对应项,`-nothinking` 后缀保留;不再对外公布 | | |
| | `model_type` 只剩 `default` | 旧配置里出现 `expert` / `vision` 也不会让请求失败,解析时按 `default` 处理 | | |
| | 上下文文件标题从 `# context_context.txt` 改为 `# 对话记录` | 旧文件是纯文本,读回不报错;新文件不再使用文件名式标题 | | |
| | 对话记录角色行从 `[r=0]`…`[r=9]` 改为 `[role=user]` 等 | 管理台解析同时接受 `[role=...]` 与旧 `[r=N]`;后端只把记录当纯文本 | | |
| | 移除工具说明文件与调用引导 | 不再生成 `context_tools.txt`、不输出 DSML 格式要求;`tools` 只影响响应侧解析,取决于 `tools.enabled` | | |
| | 移除技能注入的引导语 | 技能块只保留 `<skill>` 标签与正文,不再有“请遵循技能”这类前缀 | | |
| | 思考格式注入从 `internal/httpapi/openai/shared` 移到 `internal/promptcompat` | 四个对话接口共用同一实现,配置项与字段名不变 | | |
| | 联网信号改为「模型默认 + 客户端显式覆盖」 | 只在客户端发显式信号时改变行为;不发信号的客户端表现与之前一致 | | |
| | 新增 `tools.enabled`,缺省 `false` | 默认行为变化:未显式开启时不再注入工具说明、不再返回 `tool_calls`;老客户端带 `tools` 的请求会退化为纯对话,需要工具能力请在 `/admin/settings` 或配置文件显式开启 | | |
| | 新增 `skills` 纯文本技能库(实验性,缺省关闭) | 默认不注入,不改变现有请求的 prompt;开启后只在 `FinalPrompt` / `PromptTokenText` 最前面加一个 System 技能段,`Messages` 与对话记录不受影响 | | |
| | 内置提示词收敛到 `internal/promptcompat`(`prompt_texts.go` 为唯一来源) | 文案内容全部改写;`history` 包只保留 `currentInputFilePrompt` 这一层委托,不再自带文案。行为变化默认生效 | | |
| | 衔接指令新增「执行要求」(要求继承 + 反泄漏) | 拆分后的 prompt 变长、更结构化;`PromptTokenText` 不受影响(它统计的是对话记录全文,与实际 prompt 解耦) | | |
| | 内置提示词去掉只对交付物成立的硬性文风规则 | 对每一次请求生效的层(L1/L2/L3-`default`)不再要求「结论先行」「不写铺垫与总结段」「说明改了什么、为什么」「检查单位与口径」;这些要求要么改成带触发条件(「有这类要求就…,没有就不必…」),要么只留在用户显式开启的 `writing` 预设里。闲聊形态因此不再被压成公文 | | |
| | 修复:历史拆分后执行契约被埋在附件里 | 拆分发生时把契约重贴到可见 `FinalPrompt`(只改 prompt,不改 `Messages` / `PromptTokenText`)。关闭思考注入或未发生拆分时行为不变 | | |
| | 新增 `prompt_profile` 提示词特化层(缺省关闭) | 默认不注入,现有请求逐字节不变;开启后只改 `FinalPrompt` / `PromptTokenText`,`Messages` 与对话记录不受影响 | | |
| | 技能注入改为走 `ApplySystemOverlays` | 单片段输出与旧实现逐字节一致;只有与特化层同时开启时才合并成一个 `System:` 段 | | |
| | 新增 `prompt_style`(`v2` 缺省 / `legacy`) | 缺省仍是 `v2`,现有请求不变;切 `legacy` 时衔接指令回到短文案且不注入内置执行契约(自定义思考提示词仍生效)。未知值回落 `v2`,不会让配置加载失败 | | |
| | 新增 `model_profiles` 自定义模型列表(缺省空) | 空列表时行为与之前完全一致;条目只影响请求里出现该模型名的调用,其他请求不受影响。自定义模型名优先于内置别名映射 | | |
| --- | |
| ## 9. 相关文档 | |
| - [prompt-design.md](prompt-design.md):提示词方案(分层职责、各层文案全文、写作特化 preset 与可粘贴 skills 条目、客户端适配、调优建议) | |
| - [API.md](../API.md) / [API.en.md](../API.en.md):请求字段、响应结构、Admin 接口 | |
| - [README.en.md](../README.en.md):模型列表与部署入口 | |
| - [AGENTS.md](../AGENTS.md):适配器边界与文档同步要求 | |