LLM Agent 的系统提示词工程:动态按需加载、自愈回执与反幻觉契约
系统提示词越写越长、每次 debug 都加硬编码补丁、模型工具调用频频格式错误?本文从工程落地视角,深入分析 System Prompt 的分层渐进披露、工具 Schema 动态注入、结构化错误自愈回执以及泛化反幻觉契约的设计与实战。
在上一篇《LLM Agent 的上下文管理实践:文件、图片与指令标记的协调》中,我们讨论了对话消息正文(User / Assistant / Tool 消息流)中附件折叠、多模态分流与标记隔离的设计。但除了消息历史,LLM 每次请求的头顶还顶着一个更重、更敏感的载荷——系统提示词(System Prompt)与工具定义(Tool Definitions / Schema)。
随着 Agent 功能日益丰富(引入数十个 MCP、文件读写、深度搜索、知识库),系统提示词很容易陷入两难:写得太简略,模型工具调用频频格式错乱;写得太详尽,每轮对话常驻白白浪费数千 Token,且提示词迅速“过拟合”变成一堆难以维护的个案补丁。
本文将从真实的生产实践出发,系统拆解如何设计一套低 Token 消耗、高鲁棒性且具备自愈能力的 System Prompt 与工具交互体系。
一、 System Prompt 的三大工程陷阱
在构建真实的 Agent 系统时,很多团队最初的 System Prompt 演化路径往往是这样的:
- 初始阶段只有一两句话;
- 加了几个工具,把完整的参数格式和大段 JSON 示例写进系统提示词;
- 接入了 GitHub、Notion、Google Workspace 等 MCP,把所有工具 Schema 全量常驻挂载;
- 发现模型在某个边角场景下犯了蠢(例如模型把
/news当成了系统命令),于是在提示词里加了一行:“禁止使用 /news”; - 两个月后,System Prompt 膨胀到 4000+ Tokens,充斥着几十条互有冲突的负面规则和冗长示例。
这种演进方式会迅速踩中三大工程陷阱:
flowchart LR
A[全量常驻挂载] -->|无差别计费| E1[Token 成本与 TTFT 爆炸]
B[针对个案打补丁] -->|规则碎片化| E2[提示词过拟合 无法泛化]
C[长文本规则堆砌] -->|注意力稀释| E3[关键指令被忽略 格式频错]
1.1 常驻 Token 账单与延迟放大
LLM API 的计费是按请求的所有输入 Token 计算的。如果你的 System Prompt + 全量 Tool Schema 常驻占用了 3000 Tokens,那么用户即便只是简单问一句“你好”,也会产生 3000 Tokens 的输入计费;在一个 10 轮的普通日常对话中,仅仅是系统提示词本身就被重复传输和计费了 3 万 Tokens。更致命的是,过长的 System Prompt 会直接拖慢首字延迟(TTFT),让响应变得笨重。
1.2 针对单点问题的“提示词过拟合(Overfitting)”
当模型在测试中把某个未实现的命令当成内置命令时,直觉上的修法往往是加一条专门的负向约束:
❌ 反模式补丁:
- 严格禁止使用 /news 和 /wiki 命令!
- 不要把 set_text、str_replace 当作编辑操作名!
这种补丁是典型的过拟合:它只防住了 /news,模型下次可能还会编造 /calc、/weather;它只防住了 str_replace,模型下次还会造出 insert_text。规则越堆越多,不仅破坏了提示词的一致性,还无法真正解决同类问题。
1.3 注意力稀释与“粉色大象效应”
大量研究与实践表明,大模型对长上下文中的多重否定指令(“不要做 A,不要做 B,千万不要做 C”)极其脆弱。连续的负面穷举不仅会分散模型的注意力权重,还容易让模型更关注被禁用的词汇。
二、 双轨制架构:三层渐进式披露(Progressive Disclosure)
解决 System Prompt 膨胀与准确性矛盾的核心思想,是借鉴经典交互设计中的 渐进式披露(Progressive Disclosure)——在用户和模型不需要的时候保持极致精简,在发生错误或明确需要深入时才下发具体上下文。
我们将整个系统提示与工具协议划分为三层:
flowchart TD
Layer1["第 1 层:全局常驻契约(SSOT)\n~300 Tokens · 纯原则性法则 · 0 具体工具细节"]
Layer2["第 2 层:动态工具与指南懒加载\n按需激活 · 仅在工具开启/会话有对应实体时注入 Schema"]
Layer3["第 3 层:结构化错误回执(Self-Correction)\n参数报错时返回精确 Example · 错误触发 · 0 常态开销"]
Layer1 -->|日常轻量对话| Normal[极速低成本响应]
Layer1 -->|开启 MCP 或附件触发| Layer2
Layer2 -->|调用参数不合规| Layer3
Layer3 -->|模型读取 example 字段| Retry[下一轮精准自愈重试]
2.1 第 1 层:全局常驻契约(SSOT)
全局常驻的 System Prompt 应当只占 ~300 Tokens,它是系统的顶层法典,只承载跨领域、不可动摇的行为原则:
- 真实性与回执一致性:没有收到工具成功的真实回执(
ok: true),严禁在文本中声称文件已创建或已修改; - 工具列表权威性:只能调用当前请求的 API tools 列表中真实下发的工具,严禁输出伪造的 XML/JSON 文本标记;
- 时钟锚点(Temporal Grounding):以最新的用户时间戳作为绝对客观的“当前时间”,防止模型退化到预训练截止期。
IMPORTANT
黄金法则:第 1 层绝对不出现任何具体工具的参数名字、类型要求或具体操作步骤。这些细节属于工具自身,不属于全局。
2.2 第 2 层:动态工具与说明的按需注入
未启用的工具和 MCP 描述,1 个 Token 都不应该出现在请求中。
| 工具 / 能力类别 | 激活触发条件 | 注入内容 |
|---|---|---|
| 未授权 / 未开启的 MCP(如 Notion, GitHub, Google) | 用户未连接 OAuth 或侧边栏关闭 | 0 描述,0 Schema 注入 |
懒工具:file_read / image_understand | 仅当当前会话中真实存在文件附件或历史图片时 | 动态载入 file_read Schema 及精炼调用导引 |
懒工具:file_edit | 仅当登录用户且会话内存在**持久化文件(durable fileId)**时 | 动态载入 file_edit Schema |
| 详细产品使用说明(Detailed Product Guide) | 仅当用户明确询问“怎么用/有哪些命令”时(正则判定 wantsProductUsageHelp) | 展开详细的功能与命令手册 |
这种设计让绝大多数纯文本、非工具调用的日常对话,系统提示词始终保持在几百 Token 的极小负载内。
三、 规则泛化:用正向契约与 Schema 替代负向穷举
如何避免每次遇到 Bad Case 就往 System Prompt 里打硬编码补丁?核心在于:用“正向白名单契约”与“结构化 Schema 约束”彻底替换自然语言负向穷举。
3.1 案例对比:斜杠命令(Slash Commands)防幻觉
-
❌ 个案打补丁(无法泛化):
There is NO /news or /wiki slash command. Never tell the user to type /news or /wiki.漏洞:模型下一轮可能会让用户输入
/weather、/translate。 -
✅ 正向白名单契约(彻底泛化):
Only documented slash commands (/image, /research, /papers, /books, /skill, /review, /compact) are supported. Never invent or suggest unlisted slash commands; use web_search or built-in tools for general information retrieval.效果:一条规则封死所有未定义命令的编造路径,并指明了正向替代方案(
web_search)。
3.2 案例对比:文件编辑操作名(Op Names)防错
在支持多格式就地编辑的 file_edit 工具中,模型需要支持文本替换(replace_text)、幻灯片替换(pptx_replace_on_slide)、表格单元格设置(xlsx_set_cell)等。
- ❌ 自然语言负向禁止:
Never invent op names (not set_text, str_replace, search_replace, insert_text). - ✅ JSON Schema 的 Enum 强约束:
直接在工具的 JSON Schema 中声明完整的枚举,并在 System Prompt 中进行通用约束:
// tool definition schema properties: { ops: { items: { properties: { op: { type: 'string', enum: ['replace_text', 'delete_text', 'pptx_replace_on_slide', 'xlsx_set_cell', 'xlsx_set_cells', 'docx_set_paragraph', 'full_replace'], description: 'Exact op name from the enum (no aliases).' } } } } }
主流结构化输出模型(GPT-4o / Claude / DeepSeek)在看到 Schema 层的ops[].op must be strictly chosen from the schema enum; no aliases or unlisted names are supported.enum时,会直接在解码阶段限制采样空间,从根源上杜绝别名幻觉。
四、 错误自愈回执(Self-Correction Protocol):示例应该放在哪里?
复杂的工具调用(如生成 Excel 表格、多层嵌套的文档编辑)需要示例(Example)来辅助模型理解。但把完整的 JSON 示例常驻放在工具定义或 System Prompt 中,每轮请求都在消耗 Token。
4.1 示例的绝佳位置:错误发生时的回执(Tool Result)
更好的策略是:常态只给微型语法提示,完整的结构化示例只在模型调用出错时,由 Tool 返回给模型。
sequenceDiagram
participant User as 用户
participant LLM as 模型
participant Tool as Tool 运行时
User->>LLM: "帮我建一个上月销售业绩表"
Note over LLM: 模型尝试调用 file_write<br/>但参数漏掉了二维 rows 结构
LLM->>Tool: call file_write(mode="spreadsheet", sheets=[{name:"S1"}])
Tool-->>LLM: role: tool 返回结构化错误与精准 example
Note over LLM: 模型读取 example 字段<br/>对齐数据结构重新生成参数
LLM->>Tool: call file_write(mode="spreadsheet", sheets=[{name:"S1", rows:[["日期","金额"],[...]]}])
Tool-->>LLM: ok: true
LLM->>User: "已为您生成表格并保存!"
4.2 统一的全域自愈回执形状
不论是顶层 Schema 校验失败,还是深层业务逻辑校验失败(例如传了空的 sheets[].rows),工具均统一返回如下形状的 JSON:
{
"ok": false,
"error": "spreadsheet mode requires non-empty sheets[].rows",
"example": {
"mode": "spreadsheet",
"filename": "report.xlsx",
"sheets": [
{
"name": "Sheet1",
"rows": [
["日期", "金额"],
["2026-08", 100]
]
}
]
}
}
4.3 动态多态示例分发(避免文本关键词误伤)
对于一个同时支持纯文本与多种 Office 格式的通用编辑工具(如 file_edit),下发哪个自愈示例应当是自适应且精准的。
这里有一个极容易被忽视的细节:不能直接对入参字符串做简单的正则模糊匹配。例如,如果用户在 Python 脚本中查找替换包含 "styleSheet" 或 "cellPhone" 的变量名,简单的 /sheet|cell/ 正则会误把代码编辑当成 Excel 表格操作,错误下发 xlsx_set_cell 示例。
健壮的示例决策应当采用结构化优先原则:
export function resolveFileEditExample(
error: string,
rawArguments?: string,
): Record<string, unknown> {
// 1. 优先解析入参中的结构化 ops 操作名,避免被 text payload 中的普通单词误导
try {
const parsed = JSON.parse(rawArguments || '{}');
const ops = Array.isArray(parsed?.ops) ? parsed.ops : [];
for (const item of ops) {
const op = String(item?.op || item?.type || '').toLowerCase();
if (op.startsWith('xlsx_') || op === 'xlsx_set_cell') {
return FILE_EDIT_EXAMPLES.xlsx_cell;
}
if (op.startsWith('pptx_') || op === 'pptx_replace_on_slide') {
return FILE_EDIT_EXAMPLES.pptx_replace;
}
if (op.startsWith('docx_') || op === 'docx_set_paragraph') {
return FILE_EDIT_EXAMPLES.docx_paragraph;
}
}
} catch {
/* 忽略 JSON 解析异常 */
}
// 2. 带有严格词界的错误信息兜底判定
const err = String(error || '').toLowerCase();
if (/\b(?:xlsx|spreadsheet)\b/.test(err)) return FILE_EDIT_EXAMPLES.xlsx_cell;
if (/\b(?:pptx|presentation)\b/.test(err)) return FILE_EDIT_EXAMPLES.pptx_replace;
if (/\b(?:docx|wordprocessingml)\b/.test(err)) return FILE_EDIT_EXAMPLES.docx_paragraph;
// 3. 默认回落到通用文本替换示例
return FILE_EDIT_EXAMPLES.text_replace;
}
五、 多模态分流与系统提示词的协同
在现代多模态 Agent 中,同一套会话系统可能接入 原生多模态视觉模型(Vision-Language Models,如 GPT-4o / Claude 3.5 Sonnet),也可能接入 纯文本推理模型(如 DeepSeek-R1 / o1)。
如果在 System Prompt 中对图文能力描述不清,很容易导致两头不讨好:
- 视觉模型在多模态上下文下,会疑惑为什么工具列表里没有
image_understand工具; - 文本模型在遇到历史图片引用时,不知道应该调用懒加载工具还是放弃。
正确的系统描述契约应当明确能力与工具的分工:
Image understanding is a built-in product capability:
1. Vision models inspect images directly as pixels;
2. Logged-in text-only models auto-transcribe recent attachments server-side, and use the lazy image_understand tool for earlier turns.
Do not claim you cannot understand images merely because the tool is absent this turn.
- 服务端调度:多模态模型直接把当前轮附件作为
image_url传入;纯文本模型在请求前置拦截器完成图片 OCR 转写,历史图片则懒注入image_understand。 - 提示词对齐:系统提示词只陈述“图片理解是内置能力”,消除模型因工具缺失而产生“我无法看图”的误报。
六、 总结与工程检查清单
将系统提示词从“臃肿的手写便签”升级为“工业级契约架构”,建议对照以下检查清单:
graph TD
Check1["1. 常驻 System Prompt 是否控制在 300~500 Tokens 以内?"] --> Yes1[是]
Check2["2. 未授权/未开启的 MCP 是否实现 0 Token 零开销?"] --> Yes2[是]
Check3["3. 复杂参数示例是否已从 System Prompt 移至错误回执(Error Example)?"] --> Yes3[是]
Check4["4. 负向规则是否已重构为正向白名单契约?"] --> Yes4[是]
Check5["5. 枚举型参数是否由 JSON Schema 的 enum 承载而非自然语言硬背?"] --> Yes5[是]
- 精简常驻:System Prompt 守原则、讲协同;参数细节交由 Tools Schema 承载。
- 按需加载:只有在用户打开对应开关、或会话中实际出现关联实体时,才下发对应的 Schema。
- 错误自愈:将复杂结构示例放在
role: tool的失败回执中,用运行时错误反馈驱动模型低成本重试。 - 契约泛化:彻底告别针对单次 Bad Case 的自然语言负向修补,用正向白名单和 Schema 枚举构建经得起时间考验的稳固 Agent 体系。