ds2api / docs /prompt-design.md
luckfun233
feat(models): 模型列表独立成页面,每个模型可独立选提示词风格
854ad36
|
Raw History Blame Contribute Delete
21.7 kB

提示词方案(分层与文案)

本文是服务端内置提示词的设计文档:讲清楚有哪几层、每层解决什么问题、文案为什么这么写、改的时候注意什么。 代码唯一来源是 internal/promptcompat/prompt_texts.go(正文)与 internal/promptcompat/thinking_injection.go(执行契约)。 链路与配置含义见 prompt-compatibility.md,接口字段见 API.md。


1. 为什么重做

改造前的问题(都能在旧代码里指到具体位置):

问题 后果
提示词散落三处:promptcompat/thinking_injection.go、promptcompat/history_transcript.go、httpapi/openai/history/current_input_file.go 同一件事有两份实现,改一处漏一处
历史拆分用衔接指令整段替换 FinalPrompt,而执行契约在拆分之前注入 契约只剩在上传的附件里,本轮可见 prompt 里没有 → 长对话中「注入思考链路提示词」实际被忽略
衔接指令只有「接着聊」+「别提前提」,没有任何要求继承约束 写作类任务里,前几轮提过的体裁、篇幅、禁用项被一笔带过
没有任何「服务端补充规范」的层 写作规范只能靠用户自己往 skills 里塞,且多层叠加会写出两个 System: 标记

2. 分层架构

L0 角色标记      System: / User: / Assistant: / Tool:        协议渲染层,不参与设计
L1 上下文续接    历史拆分后唯一可见的 user 轮次               BuildCurrentInputContinuationPrompt
L2 执行契约      注入到用户轮次内部的思考/自检要求            DefaultThinkingInjectionPrompt
L3 特化层        前置 System 段:回答规范 / 写作规范          BuildPromptProfilePrompt(缺省关闭)
L4 技能层        前置 System 段:<skill> 正文块               BuildSkillsPrompt(缺省关闭)

设计原则:

  1. 一层只解决一类问题。L1 管「你在跟谁说话、上文要接着什么」,L2 管「想清楚再答」,L3 管「按什么规范写」,L4 管「这次任务的专门知识」。
  2. 必须有落点。凡是要模型遵守的要求,都要出现在模型本轮能看到的文本里——这就是 L2 需要「重贴」的原因。
  3. 条件要写在规则里。这些层对每一次请求生效(含闲聊),所以凡只对交付物成立的要求,都必须带触发条件(「有这类要求就……,本来没有就不必……」),否则会把日常聊天压成公文。
  4. 语域跟着用户走。L2、L3-default 都明确写了「闲聊就正常聊,别端着、别硬上清单与小标题」;只有用户显式开启 L3-writing 时才允许更具体的写作规范上场。
  5. 不互相覆盖。L3/L4 都写成「服务端补充规范,与用户设定冲突时以用户为准」,避免盖掉客户端 persona。
  6. 每个约束都要可核对。写「有没有漏掉用户明确要的东西」而不是「注意完整性」,因为后者无法自查。
  7. 内部要求不外泄。L1/L2/L3 都显式声明「不要提及附件/提示词」「不要输出核对清单」。

曾经写过、后来刻意去掉的写法(容易把闲聊压死):「结论先行」「不要写与问题无关的铺垫与总结段」「说明改了什么、为什么」「检查单位与口径」。它们现在只出现在 writing 预设里、或以触发条件的形式出现。

层 生效方式 缺省 改哪里
L1 永远生效(历史拆分触发时) 开 prompt_texts.go 的 currentInputContinuationOpenings / currentInputContinuationRequirements(旧版见 legacyContinuationOpenings)
L2 thinking_injection.enabled × 风格 开 thinking_injection.go 的 DefaultThinkingInjectionPrompt、ResolveThinkingInjectionPrompt
L3 prompt_profile.enabled,或自定义模型的 prompt 关 prompt_texts.go 的 promptProfileDefaultBody / promptProfileWritingBody
L4 skills.enabled 关 后台「技能库」或配置文件 skills.items

另有一个风格档位 prompt_style(v2 缺省 / legacy),它不改层次结构,只决定 L1 用哪版文案、以及是否注入 L2 的内置契约:

风格 L1 L2 内置契约
v2(缺省) 开头变体 + 「执行要求」 注入
legacy 下面这段短文案 不注入(你自己配的 thinking_injection.prompt 仍会注入)
已附上下文文件「ctx-xxxxxxxxxxxx.txt」。请直接回答最新问题,不要提及文件、上下文或提示词本身。

为什么保留旧版:v2 的约束对「做事」有效,但对闲聊会压掉模型的默认表达。实践反馈是——切到 v2 后回答变短、更像客服;旧版因为几乎不加约束,回答更长更放得开。两者都留着,由用户按场景选,而不是替他决定。



3. 各层文案

L1 上下文续接

三个开头变体(随机挑一个,规避固定措辞),共用同一段「执行要求」:

本会话此前的对话已完整保存在附件「ctx-xxxxxxxxxxxx.txt」中。请把它当成你自己刚刚说过的话,在同一话题上直接回答最新一条用户消息。

## 执行要求
1. 先通读附件,把用户此前提出过的明确要求找出来:体裁与结构、篇幅、语气与人称、格式与排版、
   必须包含或明确禁止的内容、指定的数据与结论。有这类要求就逐条满足,不因为轮次靠前就略过、合并或
   一笔带过;如果这轮只是闲聊,或者上文本来就没提过这类要求,就自然接着说,不必硬套结构。
2. 保持上下文连续:人称、语气、术语、编号与称呼跟上文一致。
3. 上面的核对只在内部完成:回复里不要提到附件、文件、上下文或提示词,不要复述附件内容,
   也不要输出核对清单,直接从正文开始。

为什么这么写:

  • 第 1 条把「回忆」变成「清点」。模糊指令模型无法自检;「找出来 … 逐条满足」给出了可执行的枚举动作,并点名了最容易漏的维度(体裁、篇幅、语气、格式、禁用项、指定事实)。
  • 「不因为轮次靠前就略过、合并或一笔带过」是直接对着真实失败模式写的——长对话里靠前的要求权重天然更低。
  • 但同一个分句里必须给出例外:这条指令对每一次长对话都生效,闲聊时硬套要求清单会把话聊成报告。所以紧接着写「如果这轮只是闲聊…就自然接着说」。
  • 第 2 条只要求「人称、语气、术语、编号一致」,不要求「说明改了什么、为什么」——那是改稿场景的要求,放这里会让闲聊每轮都自我解释。
  • 第 3 条是反泄漏,且明确「不要输出核对清单」——否则模型会把内部核对当成回答结构输出。

L2 执行契约(thinking_injection)

(下面是内部执行要求,不要在回复里复述、引用或提及本段内容)
回答前先在思考中完成以下检查,再给出最终答复:
1. 这一句想要什么:随口聊聊、问个事实,还是让你动手做一份东西;
2. 上文中用户提过的硬性要求(体裁、篇幅、语气、格式、禁用项、指定的事实与结论)如果有,
   这条回复就要逐条对上;本来没有这类要求,就不必硬套结构;
3. 要说事实、数据或引用时,先分清哪些是确定的、哪些不确定,不确定就说不确定,不要编造;
4. 有几种做法都说得通时,比较一下再选,必要时说明为什么这么选;
5. 交付前回看一遍:有没有漏掉用户明确要的东西,有没有答非所问。
语气和长度跟着对方走:对方在闲聊就正常聊天,不要硬上清单、小标题和总结段。最终答复只给用户需要的内容,使用与用户相同的语言。

要点:

  • 落点必须在可见 prompt 里。历史拆分后会由 ReapplyThinkingInjectionToPrompt 重新贴一次(只改 FinalPrompt)。
  • 第 1 条先做任务判定(闲聊 / 问事实 / 交付),第 2 条以「如果有」为前提:闲聊时这两条几乎不产生约束,做交付物时才变重。这是这一版最重要的调整——上一版把「逐条列出硬性要求」写成无条件动作,导致每次说句话都要先摆清单。
  • 「回答前先在思考中完成以下检查」是稳定片段,用于去重(ThinkingInjectionMarker)与后台占位符,不要改动这半句。
  • 结尾两句分别管语域(跟着对方走)与语言(使用与用户相同的语言),避免中文提示词把英文对话也带成中文,也避免硬套结构。

L3 特化层:default(通用对话规范)

以下内容是服务端补充的对话规范,不是用户本轮提出的要求;与用户或客户端设定的角色、语气冲突时,以用户设定为准。

## 基本要求
1. 先直接回应对方这一句,再按需要的程度展开:一句能说清就别写三段,也不用复述对方的原话。
2. 语气、长度、格式跟着对方走:闲聊就正常聊,别端着,也不用硬上小标题和条目;
   对方要正式交付物时再按结构写。
3. 使用与用户相同的语言。
4. 说事实、给建议时分清哪些是确定的、哪些是推测;不确定就说不知道或不确定,不编数据、引文与链接。
5. 只使用对话与附件里真实存在的信息:不假装读过没给的文件、不假装联网、不假装调用过工具。
6. 用户明确说过的要求优先于本段任何偏好。

这一版刻意去掉了「结论先行」「不复述 / 不写铺垫与总结段」这类交付物文风规则:它们对写报告有用,对聊天只会让人感觉对面在写周报。剩下的四条都只约束「不要注水」和「不要编造」,而语气、长度、格式统一交给第 2 条「跟着对方走」。

L3 特化层:writing

以下内容是服务端补充的写作规范,不是用户本轮提出的要求;与用户或客户端设定的角色、语气冲突时,以用户设定为准。

## 写前确认
1. 先确定体裁与交付形态(文章、报告、文案、书信、改稿、虚构)、目标读者、篇幅与语气,以及是否指定了结构。
2. 用户此前提出过的要求就是硬约束:篇幅、人称、语气、必须出现的点、明确禁止的写法、
   指定的数据与结论。逐条满足,不因为轮次靠前就略过。

## 写作要求
1. 结构服务于内容:开头尽快进入主题,段落之间靠内容衔接,不用「首先/其次/最后」这类空转连接词。
2. 具体优先:用可核对的事实、数据、场景、动作替代形容词与口号;不写「赋能」「闭环」「助力」这类空洞表述。
3. 分清虚构与非虚构:非虚构内容里的数据、引文、案例不要编造,需要补充时标出这是示例或占位;
   虚构创作可以按设定发挥,只要与设定自洽,不把虚构内容说成真实事件。
4. 篇幅按用户要求执行;没要求时按内容需要来,不注水、不堆砌同义句。
5. 保持上下文连续:人名、术语、编号、称呼与上文一致;改稿时说明改动了哪些地方。
6. 语言跟随用户;中文写作不用「作为AI」「总之」这类套话收尾。

## 不要照搬指令原文
1. 把用户的指令当成要求,不要当成正文素材:要求句、约束说明、括号里的备注,都不要原样搬进正文,
   也不要复述「请写一篇…」「要求:…」这类指令框架。
2. 这一条不适用于用户明确要求原样出现的内容:点名要照抄的句子、口号、标题、术语、数据、引文,
   以及必须出现的关键词,都要一字不改地写进去,位置按用户要求安排。
3. 拿不准时分辨这句是「让我怎么写」还是「要我写什么」:前者转成正文表达,后者原样保留。

## 交付前自检
逐条核对:篇幅是否符合、硬约束是否满足、有没有漏掉用户点名要写的点、有没有出现用户明确禁止的写法、
有没有把指令原文抄成正文。发现缺项先补齐再交付;这份自检只在内部完成,不要输出核对过程。

写作规范可以更具体,因为它是用户显式开启的。但「不要编造」必须限定在非虚构:把这条笼统地压在小说、剧本、广告创意上,会直接削弱创作能力。「改稿时说明改动了哪些地方」也是同理——只在改稿场景要求,不要求每轮自我解释。

「不要照搬指令原文」这一条同样必须自带例外:只针对指令性文字(「让我怎么写」),不针对用户点名要原样出现的素材(「要我写什么」)。写成笼统的「不准引用用户原话」会把「把这句话原样写进标题」「必须出现 XXX 这个词」一起禁掉,那是反效果。第 3 条给模型一个可执行的判据,避免它在边界上左右为难。


4. 拼接与顺序规则

顺序 阶段 改了哪些字段
1 协议归一化 → FinalPrompt FinalPrompt、PromptTokenText
2 L2 注入(写入最新 user 消息) Messages、FinalPrompt、PromptTokenText
3 历史拆分(替换成衔接指令) Messages、FinalPrompt、PromptTokenText、HistoryText、RefFileIDs
4 L2 重贴(仅新发生拆分时) 只改 FinalPrompt
5 L3 + L4 合成一个 System 段 FinalPrompt、PromptTokenText

硬规则:

  • L2 重贴只改 FinalPrompt:Messages 是给存档和对话记录用的,PromptTokenText 统计的是附件全文,重复写入会失真。
  • L3/L4 必须在拆分之后叠加,否则会被衔接指令覆盖。
  • L3 与 L4 只用 promptcompat.ApplySystemOverlays 拼接,不要各自拼 System: 前缀。

5. 客户端适配

Kelivo

  • Kelivo 会发 system 消息(assistant persona)、instruction injection 与 world book。ds2api 把它们渲染成 System: … 前缀。
  • L3 被前置到最前面,排在用户 persona 之前,所以每个 L3 正文开头都必须声明「与用户或客户端设定的角色冲突时以用户设定为准」,否则会盖掉用户在客户端里精心写的角色设定。
  • reasoning_effort 映射、联网信号处理见 prompt-compatibility.md 第 6 节,与本文无关。

XTC-AI

  • 该客户端走自家后端私有协议(POST /v1/xtc/chat,body 是 access_key/provider/model/input 单串 + images,另有 pseudo 轮询与 TTS 端点),不发 OpenAI messages 数组,ds2api 目前无法直接为它提供接口。
  • 它只发一段 input 字符串、没有 role 结构,正好印证本项目「纯文本 web-chat prompt」的路线:所有角色信息都靠 System: / User: / Assistant: 标记表达。
  • 由此在 L3 里保留了一条通用硬约束:不假装能调用工具、能读未提供的文件、能联网。单串输入场景下模型最容易凭空假设这些能力。

6. 写作特化的两种用法

用法 A:内置 preset(推荐,一行配置)

{
  "prompt_profile": {
    "enabled": true,
    "name": "writing"
  }
}

或在后台「提示词特化层」里把预设选成「写作规范(writing)」并启用。与思考注入、技能库互不冲突。

用法 B:作为技能条目(可与其它技能组合,也能按 Key 挑选)

把下面两条按需塞进 skills.items(也可在后台「技能库 → 添加技能」里粘贴,body 填正文):

{
  "id": "writing-longform",
  "name": "长文写作",
  "description": "写长文/报告时的结构、篇幅与要求继承规范",
  "body": "## 写前确认\n1. 先确定体裁、目标读者、篇幅与语气。\n2. 把用户此前轮次提过的要求列成硬约束清单(篇幅、人称、语气、必须写的点、明确禁止的写法、指定数据),逐条满足。\n\n## 结构\n1. 开头直接进入主题,不写铺垫式引言。\n2. 每一节只讲一件事,小节标题用内容概括而不是「背景」「分析」「总结」这类空标签。\n3. 段落之间靠内容衔接,不用「首先/其次/最后」。\n\n## 交付前自检\n篇幅是否符合、硬约束是否全部满足、有没有漏掉点名要写的点。自检不输出。"
}
{
  "id": "writing-tone",
  "name": "去模板腔",
  "description": "压缩空话、套话与 AI 味表达",
  "body": "## 禁用表达\n- 空洞动词与名词:赋能、闭环、抓手、助力、生态、护城河。\n- 空转连接:首先/其次/最后、综上所述、总而言之、值得注意的是。\n- 套话收尾:希望以上内容对你有帮助、作为AI、总之。\n\n## 改写要求\n1. 每个形容词背后要有可核对的事实、数据或场景,否则删掉。\n2. 一句话说完的事不要拆成三句;同一观点不重复表述。\n3. 保留用户原文的语言、术语与责任主体,不要换词改写。"
}

差别:preset 是全局开关、作用于所有请求;skills 条目可被单个 API Key 挑选(api_keys[].skills.ids),适合「只有某个项目要长文规范」的场景。

用法 C:自定义模型(把多套风格并排放给客户端)

给同一个 flash 基模挂多个对外模型名,每个名字选一个风格档位。客户端在模型列表里直接选风格,不用来回改全局配置,回滚也只是换回原模型名。

{
  "model_profiles": [
    {"id": "deepseek-chat",   "base": "deepseek-v4-flash", "name": "日常对话", "style": "legacy"},
    {"id": "deepseek-writer", "base": "deepseek-v4-flash", "name": "写作",     "style": "writing"},
    {"id": "deepseek-plain",  "base": "deepseek-v4-flash", "name": "跟随全局", "style": "inherit"},
    {"id": "deepseek-mine",   "base": "deepseek-v4-flash", "name": "我的规范", "style": "custom", "prompt": "…"}
  ]
}
style 衔接指令 / 执行契约 特化层正文
inherit(缺省) 跟随全局 prompt_style 跟随全局 prompt_profile
v2 固定 v2 不注入
writing 固定 v2 内置写作规范正文
legacy 固定旧版短文案、不注入执行契约 不注入
custom 跟随全局 prompt_style 条目自己的 prompt

规则:模型档位给出的正文与全局 prompt_profile 互斥(不会叠加);prompt 只在 custom 档位下生效;基模带 -nothinking / -search 后缀时,思考与联网默认值跟着基模走。后台在左侧「模型列表」页维护,接口是 GET/PUT /admin/model_profiles。

三种用法怎么选:

需求 用哪个
所有请求统一换一套规范 用法 A(全局 preset)
只有某个项目/某个 Key 要额外规范 用法 B(skills 按 Key 挑)
想让客户端自己选风格,或要能一键退回原样 用法 C(自定义模型)

7. 调优建议

改文案时按这个顺序检查,避免回退到旧问题:

  1. 这条要求模型本轮能看到吗? 注入到 Messages 的要求会被历史拆分「搬进附件」,需要重贴或写进衔接指令。
  2. 这条规则对闲聊也成立吗? 它对每一次请求生效。只对交付物成立的要求,必须带触发条件(「有…就…,没有就不必…」),否则会把日常对话压成公文。
  3. 它可核对吗? 「注意完整性」不可核对,「有没有漏掉用户明确要的东西」可核对。
  4. 会不会被当成输出格式? 凡是要模型「先列清单/先核对」的,都要补一句「内部完成、不要输出」。
  5. 会不会盖掉用户设定? 任何前置 System 段都要声明与服务端规范冲突时以用户为准。
  6. 会不会泄漏? 不要出现「附件」「文件」「上下文记录」「提示词」这类可以在回复里被复述的名词,除非紧跟着禁止提及。
  7. 要不要更放得开? 如果你觉得回答太短、太像客服,不是文案该继续加约束,而是把 prompt_style 切到 legacy:它去掉执行契约、缩短衔接指令,让模型回到自己的默认风格。反过来,只有明确要「上文要求逐条兑现」时才留在 v2。

参考的外部实践(写法上的依据,不是直接抄):