# 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
…body…
```
注意:技能块里**不再有引导语**(没有“以下是技能说明/适用于当前任务时请遵循/不要提及技能”之类前缀),只保留 `` 结构化标签与正文——避免模型被引导去“使用技能/调用能力”。
为什么用纯文本而不是工具调用:工具调用要求模型吐 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` |
| 移除技能注入的引导语 | 技能块只保留 `` 标签与正文,不再有“请遵循技能”这类前缀 |
| 思考格式注入从 `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):适配器边界与文档同步要求