03 Prompt 工程
“Prompt 工程”听起来像是找几个神奇的咒语,其实在 Agent 开发里它更像写一份给新同事的工作说明:环境是什么、流程怎么走、哪些不能做、做到什么算完成。写得含糊,模型就只能猜;写得太死,遇到没写到的情况又不会变通。更重要的是,提示词改了之后行为变没变、变好还是变坏,要靠评测和运行记录来判断,而不是看几个例子觉得不错。
这篇要讲清楚:提示词出现在哪些地方(不只是系统提示词);系统提示词怎么分段;什么叫”合适的高度”;怎么写清楚、给理由、给示例、用标签分隔;工具描述和报错信息也是提示词;怎么根据失败案例迭代;提示词怎么做版本管理和回归评测;哪些事提示词做不到、必须交给代码。用我项目的系统提示词逐段拆解,并从 runs.db 里找出提示词改动前后模型行为的真实变化。
怎么读:
前置:01 大模型基础、02 模型 API 工程。
第一部分 速记页
| 问题 | 一句话答案 |
|---|---|
| 提示词在哪里 | 系统提示词、用户消息、工具名和描述、参数描述、工具返回的结果和报错信息,都会影响模型 |
| 系统提示词常见结构 | 角色和任务、环境、标准流程、限制、要点、完成标准、输出要求 |
| 合适的高度 | 太死:把复杂逻辑硬编码成规则,脆弱;太虚:只给空泛指导,没有具体信号。要具体到能指导行为,又留有判断余地 |
| 从哪开始写 | 先用最少的提示词测,再根据失败案例逐条补充说明和示例 |
| 写清楚的要点 | 具体说要什么格式和约束;步骤有顺序时用编号;说明为什么;说要做什么而不只是不要做什么 |
| 为什么要给理由 | 模型能从理由推广到没写到的情况;Anthropic 文档的例子是解释”会被语音朗读”比单说”不要用省略号”效果好 |
| 示例(few-shot) | 给 3 到 5 个相关、多样的示例,用 <example> 标签包起来,和指令区分开 |
| 分隔标签 | 用 XML 标签或 Markdown 标题把指令、背景、示例、输入分开,减少误解 |
| 长文档放哪 | 长资料放前面,问题和指令放后面;Anthropic 文档说测试中能提升回答质量最多 30% |
| 工具描述也是提示词 | 我项目把知识库工具描述从”不确定时调用”改成”每次失败都先查”,调用从 3 次到 5 次 |
| 提示词和实际环境要一致 | 我项目白名单去掉 cat 后提示词还说能用,模型会反复撞墙,专门修过 |
| 迭代方法 | 跑评测 → 读失败轨迹 → 归类原因 → 改提示词或工具或代码 → 回归评测 |
| 版本管理 | 提示词和代码一起进 git;评测结果里记录提示词版本;改提示词要跑回归 |
| 提示词做不到的 | 安全边界、格式保证、结果验收;这些必须由代码保证 |
| 我项目的真实数据 | 提示词让传 -DCMAKE_TOOLCHAIN_FILE 时 60 次运行都手动传了(113 次);改成”不用传”后 60 次里只有 1 次 |
第二部分 易混对照
| 容易混的两个 | 区别 | 一句话记法 |
|---|---|---|
| 系统提示词 vs 用户消息 | 系统提示词是开发者定的长期规则;用户消息是这次的具体任务 | 员工手册 vs 今天的工单 |
| 规则 vs 启发 | 规则是”必须这样”;启发是”遇到这类情况通常这样想” | 红线 vs 经验 |
| 太死 vs 太虚 | 太死是把 if-else 写进提示词;太虚是”你是专家,请尽力” | 操作手册抄代码 vs 空口号 |
| 零样本 vs 少样本 | 零样本只给指令;少样本还给几个输入输出示例 | 只讲要求 vs 给范文 |
| 示例 vs 规则清单 | 示例展示期望的样子;规则清单列一堆边界情况。Anthropic 建议给多样的典型示例,而不是堆边界情况 | 看样板 vs 背条款 |
| 提示词约束 vs 代码约束 | 提示词是概率性的,模型可能不遵守;代码是确定的 | 请求 vs 强制 |
| 提示词工程 vs 上下文工程 | 前者关注怎么写指令;后者关注每一步放进上下文的所有内容(06 篇) | 写说明书 vs 管整个工位 |
| 思维链提示 vs 推理模型 | 前者在提示词里要求一步步想;后者模型自带思考(01 篇) | 被要求 vs 天生 |
| 提示词迭代 vs 过拟合评测集 | 前者根据失败原因改进;后者针对评测集里的具体题目打补丁 | 改方法 vs 背答案 |
| 提示词注入 vs 提示词泄露 | 注入是让模型执行攻击者的指令;泄露是提示词内容被套出来(11 篇) | 劫持 vs 偷看 |
第三部分 面试口述稿
3.1 “你怎么写系统提示词?”
我把系统提示词当成给一个聪明但完全不了解情况的新同事写的工作说明。
结构上一般分几段:任务是什么;环境是什么,比如工作目录、有哪些工具、有哪些已经配好的东西;标准流程,按顺序编号;限制,哪些命令不能用、哪些路径不能碰;经验要点,遇到常见问题怎么想;完成标准,做到什么算成功;最后是输出要求。
写的时候注意几点。一是具体,不说”注意安全”,而说”只能访问工作目录内的路径”。二是给理由,比如”主机上的库不能用于交叉编译”,模型知道原因后,遇到没写到的情况也能推断。三是高度要合适,Anthropic 的上下文工程文章里讲过两个极端:一个是把复杂逻辑硬编码成规则,很脆弱;一个是空泛的指导,没有具体信号。四是从最少的内容开始,根据失败案例一条条加,而不是一开始就堆满规则。
我项目的系统提示词大概四十行,里面好几条都是跑评测看到失败轨迹后加进去的。
3.2 “你怎么知道改提示词有没有用?”
靠评测和运行记录,不靠看几个例子。
我项目有两个例子可以说明提示词改动确实改变了行为,也说明它的局限。
第一个是知识库工具的描述。原来写的是”不确定怎么处理时调用”,第一版评测两百多次工具调用里只查了 3 次;改成”每次 configure 或 build 失败后都先查一次”,调用变成 5 次。行为确实变了,但绝对量还是很低,而且那一轮同时改了三处,不能单独归因。说明提示词引导有效但有限,模型还是倾向于相信自己。
第二个是 toolchain 的传递。v2 的提示词标准流程里写着要传
-DCMAKE_TOOLCHAIN_FILE参数,60 次运行全部手动传了,一共 113 次;v3 改成用环境变量,提示词说”已经自动使用,不需要再传”,60 次里只有 1 次还在传。这个变化非常明显,但它和环境变量的改动是一起做的,所以严格说这是”提示词加环境一起改”的效果。所以我的做法是:改提示词要有明确要验证的行为,改完跑回归评测,从运行记录里统计那个行为变没变,同时看通过率、步数有没有变差。
3.3 “提示词和代码怎么分工?”
原则是:要保证的事交给代码,要引导的事交给提示词。
安全边界必须是代码。我项目提示词里写了”只能访问工作目录内的路径”,但真正起作用的是工具里的路径检查;评测里模型照样去
ls /opt/homebrew,是被代码拦下来的。提示词写了模型不一定遵守,11 篇讲的注入更是能直接改变模型行为。结果验收也必须是代码。提示词里写了成功判据,让模型知道目标是什么;但判断到底成没成功,是程序独立重新构建去检查。
格式保证尽量交给 API 的结构化输出或工具调用,程序再校验。
提示词擅长的是:告诉模型环境和上下文、推荐的流程、遇到某类问题怎么想、什么时候该用哪个工具。还有一点很重要,提示词要和代码实际的行为一致。我项目有一次把
cat从命令白名单删掉了,但提示词里还写着能用,模型就会一直尝试、一直被拒,后来专门提交了一次修正让两边对齐。
3.4 “few-shot 你怎么用?”
示例是控制输出格式、语气、结构最可靠的方法之一。Anthropic 文档的建议是给 3 到 5 个,要和实际场景相关、彼此足够不同,避免模型学到你没打算让它学的规律,并用
<example>标签包起来和指令分开。但示例也有代价:占上下文,而且模型可能过度模仿示例的具体内容。Anthropic 的上下文工程文章建议挑选多样的、有代表性的典型示例,而不是把一堆边界情况塞进去。
我项目的系统提示词没有用 few-shot,原因是交叉编译每个项目路径差异很大,给几个具体项目的操作序列当示例,模型很可能照搬不适用的命令。我用的是”要点”这种启发式说明,比如”报错提到警告被 -Werror 变成错误时,通常是语言标准或警告选项和工具链不匹配”。如果是信息抽取、分类、固定格式输出这类任务,我会优先用示例。
3.5 “提示词怎么做版本管理?”
提示词就是代码,和代码放在一起进 git,改动走代码审查。
评测结果里要能对应到提示词版本。最简单的做法是对提示词内容算一个哈希,写进每次运行记录和评测结果文件。这样看到结果变化时,能确认是不是提示词变了。我项目的评测结果里记录了模型、是否开知识库、步数上限、被测项目的 commit,但没有单独记录提示词版本,只能靠 git 历史和报告日期对应,这是一个缺口。
改提示词要跑回归评测,原因是提示词的影响是全局的:为了修一个问题加的一句话,可能让模型在别的场景过度执行这条规则。
第四部分 逐个详解
4.1 提示词不只是系统提示词
模型每一步看到的全部内容都会影响它的行为:
flowchart TD
subgraph 每一步发给模型的内容
SYS["系统提示词<br/>角色 环境 流程 限制 完成标准"]
TOOLS["工具定义<br/>工具名 描述 参数描述"]
USER["用户消息<br/>任务 + 项目探测结果"]
HIST["历史<br/>模型之前的调用"]
RES["工具结果<br/>命令输出 报错信息 拒绝原因"]
end
SYS --> M[模型的下一步决定]
TOOLS --> M
USER --> M
HIST --> M
RES --> M
| 位置 | 我项目里 | 影响的例子 |
|---|---|---|
| 系统提示词 | build_agent.py 的 SYSTEM_PROMPT | 标准流程、命令限制、成功判据 |
| 工具描述 | tools.py 的 SCHEMAS、KB_SCHEMA | 知识库描述改写后调用次数变化 |
| 参数描述 | run_command 的 timeout 参数”超时秒数,默认 60” | 配合提示词”configure 传 180,build 传 300” |
| 用户消息 | ”把这个项目交叉编译到 {target}” + survey 的项目探测结果 | 模型第一步看哪些文件 |
| 工具结果 | 命令输出、截断说明、分页提示 | ”用 offset=101 继续读”让模型知道怎么翻页 |
| 报错信息 | 路径越界时”主机上的库属于本机平台,不能用于交叉编译” | 引导模型换方向(04 篇) |
Anthropic 的 Building effective agents 附录里专门讲了工具的提示词:建议像给初级开发者写文档一样写工具说明,并做防呆设计(04 篇引用过)。工具描述、参数名、报错信息,在 Agent 里的分量不比系统提示词轻。
4.2 系统提示词逐段拆解
我项目当前的系统提示词(agent/build_agent.py):
你的任务是把一个 C/C++ 项目交叉编译到目标平台。
环境:
- 工作目录就是项目根目录,你只能在里面操作
- CMake toolchain 文件在 .xbuild/zig.cmake,用 zig cc 做交叉编译
- 环境变量 CMAKE_TOOLCHAIN_FILE 已经指向它的绝对路径,任何 cmake configure(包括给依赖单独建的
子目录)都会自动使用,不需要再传 -DCMAKE_TOOLCHAIN_FILE
- 目标平台由环境变量决定,已经配置好,不要改 toolchain 里的编译器路径
标准流程:
1. 先看项目结构和 CMakeLists.txt,了解构建选项
2. cmake -B build [其他选项]
3. cmake --build build -j4
4. 失败就读报错,判断原因,调整 cmake 选项或打补丁,重来
命令限制(重要):
- 每次只能执行一条命令,不支持 && || | 和重定向,写了也不会生效
- 可用命令只有 cmake make ninja git ls file nm ldd readelf,没有 cat(读文件用 read_file 工具)
- 只能访问工作目录内的路径,主机上的库和头文件不能用于交叉编译
- 要删除 build 目录用 cmake -E rm -rf build,没有 rm 可用
- 想看文件内容直接用 read_file 工具,不要用 cat 或 git grep 绕
- 编译命令要给足超时,configure 传 180,build 传 300
要点:
- 优先通过 cmake 选项解决,尽量不改项目源码
- 关掉测试、示例、共享库这类非必需目标可以减少麻烦,但不能把所有编译目标都关掉;
header-only 项目至少保留一个会被编译的测试或示例
- 报错提到某个编译警告被 -Werror 变成错误时,通常是语言标准或警告选项和
工具链不匹配,试 -DCMAKE_C_STANDARD / -DCMAKE_CXX_STANDARD,或关掉该项目
自己的严格检查选项
- 同一个办法失败两次就换思路,不要重复
- 每次 configure 或 build 失败后,先用 search_knowledge 查经验库再动手改
- 成功的判据:对项目根目录配置出的构建目录执行 cmake --build 返回 exit=0,并且那个目录里产出了
目标架构的库、可执行文件或目标文件。没用 toolchain 编出来的主机产物不算。构建目录默认用 build,
项目里已经有同名源码目录时换一个名字即可
完成后用一句话说明结果;失败就说清楚卡在哪一类问题上。
逐段看它在做什么、为什么这么写:
| 段落 | 作用 | 值得注意的写法 |
|---|---|---|
| 第一行 | 任务 | 一句话,不堆”你是资深专家” |
| 环境 | 告诉模型它在什么地方、什么已经配好 | ”已经配置好,不要改”:告诉它哪些不用操心,减少无效操作 |
| 标准流程 | 推荐路径,编号表示顺序 | 只给主干四步,不写所有分支;第 4 步”判断原因”留给模型 |
| 命令限制 | 和工具层实际能力一致 | 每条都带替代方案:“没有 rm → 用 cmake -E rm -rf""没有 cat → 用 read_file”;并说明”写了也不会生效”,而不只是”不要写” |
| 要点 | 经验和启发 | ”通常是……试……或……”:给思路而不是给唯一答案;“不能把所有编译目标都关掉”带了原因 |
| 成功判据 | 让模型知道目标 | 和程序验收的标准一致;包括边界情况(主机产物不算、构建目录重名怎么办) |
| 最后一行 | 输出要求 | 失败时”说清楚卡在哪一类问题上”,方便事后分类 |
这份提示词里好几条能在 git 历史里找到来历:
| 条目 | 来历 |
|---|---|
| ”没有 cat(读文件用 read_file 工具)”、“只能访问工作目录内的路径,主机上的库和头文件不能用于交叉编译” | 提交 80cbff2:cat 已经从白名单删掉,但提示词还宣称能用,提交信息里说”会让模型反复撞墙”;同时把”主机的库不能用于交叉编译”提前写进提示词,而不是等模型去 ls /opt/homebrew 时才从拒绝信息里学到 |
| ”环境变量 CMAKE_TOOLCHAIN_FILE 已经指向……不需要再传” | v3 的改动。之前第 2 步写的是 cmake -B build -DCMAKE_TOOLCHAIN_FILE=.xbuild/zig.cmake |
| ”每次 configure 或 build 失败后,先用 search_knowledge 查经验库” | 提交 322edb2。原来是”报错看不懂或不确定怎么处理时,可以用 search_knowledge 查经验库" |
| "header-only 项目至少保留一个会被编译的测试或示例”、成功判据里的”产出了目标架构的……” | 提交 1941ce9,和新的验收标准一起加入。原来的判据只写”cmake —build 返回 exit=0" |
| "报错提到某个编译警告被 -Werror 变成错误时……” | 提交 829cc94。runs.db 里 run 2 编 cJSON 时报错里有 -Werror,状态是步数用完;同一次提交的说明里记录 cJSON 最终 13 步通过、“自行定位 -std=c89 -Werror 与 musl 冲突”。两件事的先后顺序从记录里没法完全确认 |
这份提示词的问题(也要能说出来):
- “同一个办法失败两次就换思路”和代码里重复调用超过 3 次才拦截,数字不一致
- “每次 configure 或 build 失败后先查经验库”是很强的要求,但实际调用率仍然很低(留出集 30 次运行 8 次),说明模型并没有照做,写了不等于做了
- 没有说明知识库查不到相关内容时怎么办
- 没有版本标识
4.3 合适的高度
Anthropic 的 Effective context engineering for AI agents(2025-09)把系统提示词的问题描述成两个极端:
flowchart LR
A["太死<br/>把复杂、脆弱的逻辑<br/>硬编码进提示词<br/>想精确控制每个行为"] --- M["合适的高度<br/>具体到能有效指导行为<br/>又足够灵活<br/>给模型有力的启发"] --- B["太虚<br/>空泛的高层指导<br/>没有具体信号<br/>或者默认模型知道背景"]
用我项目的场景举例:
| 太死 | 合适 | 太虚 |
|---|---|---|
”如果报错包含 Could NOT find ZLIB,执行 cmake -B build -DZLIB_ROOT=deps/zlib;如果包含 fatal error: png.h……”(写成一张查表) | “报错提到某个编译警告被 -Werror 变成错误时,通常是语言标准或警告选项和工具链不匹配,试……或……" | "你是交叉编译专家,请认真分析报错并解决” |
| 遇到表里没有的报错就不知道怎么办;报错措辞稍变就匹配不上 | 给了原因和几个方向,模型能推广到类似情况 | 模型只能靠自己的通用知识猜 |
同一篇文章里的另外几条建议:
- 用 XML 标签或 Markdown 标题把提示词分成清楚的几段(背景、指令、工具说明、输出说明)
- 追求能完整描述期望行为的最少信息,但”最少”不等于”最短”,该给的信息要给够
- 先用最少的提示词测试,再根据失败模式补充指令和示例
- 示例要挑选多样的、有代表性的,而不是把一长串边界情况塞进去
“根据失败模式补充”就是我项目那几条要点的来历。 反过来,这也意味着提示词里每一条都应该能回答”它是为了解决什么问题加的”,回答不上来的可以考虑删掉。
4.4 写清楚:具体、给理由、说该做什么
Anthropic 的 Prompting best practices 文档里有几条很实用:
1. 把模型当成聪明但不了解情况的新员工。 文档给了一个”黄金法则”:把提示词给一个不了解这个任务的同事看,让他照着做,如果他会困惑,模型也会。
2. 具体说明输出格式和约束;步骤的顺序或完整性重要时,用编号列表。
3. 给出指令背后的原因。 文档里的例子:
| 效果较差 | 效果较好 |
|---|---|
| 永远不要用省略号 | 你的回答会被语音合成引擎朗读,所以不要用省略号,因为引擎不知道怎么读它 |
文档的说法是模型能从解释中推广。放到我项目:
| 只有指令 | 带原因 |
|---|---|
| 不要访问工作目录外的路径 | 只能访问工作目录内的路径,主机上的库和头文件不能用于交叉编译 |
不要用 && | 每次只能执行一条命令,不支持 &&、管道和重定向,写了也不会生效 |
第一条的原因让模型明白”去主机上找 zlib”这个方向本身就是错的,而不只是”那个路径被禁止了”。
4. 说要做什么,而不只是不要做什么。 文档在”控制输出格式”一节里的第一条就是这个。我项目命令限制里每一条”没有 X”后面都跟着”用 Y”。只说”不能用 rm”,模型可能换个方式绕;说”删除 build 目录用 cmake -E rm -rf build”,模型直接知道正确做法。
4.5 分隔标签和长文档的位置
用标签把不同类型的内容分开。 同一份文档说,提示词里混着指令、背景、示例、变量输入时,用 XML 标签分别包起来(比如 <instructions>、<context>、<input>),能减少误解;标签名要一致、有描述性;内容有层级时可以嵌套,比如多个文档放在 <documents> 里,每个是 <document index="n">。
<instructions>
根据下面的报错,判断属于哪一类问题,只输出类别名。
</instructions>
<categories>
dependency_missing, header_missing, link_error, arch_mismatch, compile_error
</categories>
<error_log>
fatal error: zlib.h: No such file or directory
</error_log>
标签的另一个作用是区分”指令”和”数据”。 工具结果、检索到的文档、用户上传的文件包在标签里,并告诉模型”标签里的内容是数据”。这能降低提示词注入的成功率,但不能保证(11 篇)。
长文档放前面,问题放后面。 文档说处理大量资料(2 万 token 以上)时,把长文档和输入放在提示词靠前的位置、问题和指令放在后面,测试中能让回答质量提升最多 30%,尤其是多文档的复杂输入。
这和提示词缓存(06 篇)的要求也不冲突:稳定的系统提示词在最前面,变化的长资料其次,每次都不同的具体问题在最后。
4.6 示例(few-shot)
先说为什么会有 few-shot 这种用法。 在 GPT-3 之前,想让语言模型做一个具体任务(比如把报错分类),标准做法是微调:先标注几千到几万条”输入 → 答案”的数据,再拿它继续训练模型,每换一个任务就要重新标、重新训。2020 年 5 月,OpenAI 的 Tom Brown 等人发表 GPT-3 的论文 Language Models are Few-Shot Learners,展示了 1750 亿参数的模型不用改任何参数,只要在输入文字里写上任务说明和几个例子,就能照着做,有些任务上能追平当时微调过的最好方法。论文把这种做法按例子个数分成零样本、单样本、少样本。从这以后,“输入怎么写”本身成了影响效果的关键,提示词工程就是从这里开始被当回事的。
少样本提示(few-shot prompting):在提示词里给几个”输入 → 期望输出”的例子,让模型照着这个模式做。只给指令不给例子叫零样本(zero-shot)。
Anthropic 的 Prompting best practices 文档说,示例是控制输出格式、语气和结构最可靠的方法之一,建议:
| 要求 | 含义 |
|---|---|
| 相关 | 和实际使用场景接近 |
| 多样 | 覆盖不同情况,差异足够大,避免模型学到你不想要的规律 |
| 结构化 | 每个示例用 <example> 包起来,多个示例放在 <examples> 里,和指令区分开 |
| 数量 | 3 到 5 个效果最好 |
“避免学到不想要的规律”是什么意思? 比如三个报错分类的示例,答案恰好都是 dependency_missing,模型可能倾向于总是回答这个类别;三个示例的输入都很短,模型遇到长日志可能表现变差。
一个报错分类的示例写法:
<instructions>
判断报错属于哪一类,只输出类别名。
</instructions>
<examples>
<example>
<error>Could NOT find ZLIB (missing: ZLIB_LIBRARY ZLIB_INCLUDE_DIR)</error>
<category>dependency_missing</category>
</example>
<example>
<error>ld.lld: error: undefined symbol: LZ4_decompress_safe</error>
<category>link_error</category>
</example>
<example>
<error>file format not recognized; treating as linker script</error>
<category>arch_mismatch</category>
</example>
</examples>
<error>{待分类的报错}</error>
三个示例的类别各不相同,报错来自不同阶段(配置、链接、架构)。示例里的报错是按项目 errors.py 的分类规则挑的典型写法。
什么时候不适合给示例:
| 情况 | 原因 |
|---|---|
| 每个任务的正确做法差异很大 | 模型可能照搬示例里不适用的具体操作 |
| 示例很长 | 占上下文,每一步都要重发(01 篇) |
| 期望模型有创造性 | 示例会限制输出的多样性 |
我项目为什么没用示例:交叉编译每个项目的依赖、选项、报错差异很大,如果给”编 libpng 的 10 步操作”作为示例,模型编 re2 时可能照着去找 zlib。所以用的是”要点”这种启发式说明。知识库的作用其实就是”按需给示例”:只在遇到相关报错时,检索出对应的经验放进上下文(08 篇)。
4.7 工具描述也是提示词
工具描述决定模型什么时候调用、怎么调用。 我项目知识库工具的描述改过一次(提交 322edb2):
| 描述 | 系统提示词里对应的一句 | |
|---|---|---|
| 改之前 | 在交叉编译经验库里检索。编译或配置报错且你不确定怎么处理时调用它,把报错信息原文作为查询。不要在没有报错的时候调用。 | 报错看不懂或不确定怎么处理时,可以用 search_knowledge 查经验库 |
| 改之后 | 交叉编译经验库。每次 configure 或 build 失败后都先查一次,把报错的关键行作为查询,再决定怎么改。它会给出这类报错的成因和处理办法,能省掉大量试错。没有报错时不要调用。 | 每次 configure 或 build 失败后,先用 search_knowledge 查经验库再动手改 |
改之后多了什么:
- 把”不确定时”改成”每次失败后”。“不确定”由模型自己判断,而模型倾向于认为自己确定
- 说明了好处:“能省掉大量试错”,这是 4.4 节说的”给理由”
- 查询怎么写:从”报错信息原文”改成”报错的关键行”,避免把整段日志塞进查询
结果(REPORT 第一版):调用次数从 3 次变成 5 次。REPORT 的结论是”提示层面的引导有效但有限”:10 个项目、两百多次工具调用里只有 5 次。而且那一轮(rag2)同时改了三处(补知识库、改引导、修安全漏洞),这个 3 → 5 不能完全归因于描述的改动。
如果提示词引导不够,就改机制。 REPORT 里提出的方向是:失败后自动检索、把结果附在工具输出后面,而不是留给模型决定调不调。这是一个很典型的判断:当你发现要在提示词里越写越重地强调某件事,模型还是不做,就该考虑用代码让它必然发生。
flowchart LR
subgraph 靠提示词
A1[命令失败] --> A2[模型读报错]
A2 --> A3{模型决定<br/>要不要查}
A3 -->|多数时候不查| A4[自己改]
A3 -->|少数时候| A5[调 search_knowledge]
end
subgraph 靠机制
B1[命令失败] --> B2[程序自动检索]
B2 --> B3[报错 + 相关经验<br/>一起作为工具结果返回]
B3 --> B4[模型读到经验再决定]
end
“靠机制”没有在项目里实现,也没有评测数据,是一个待验证的改进方向。它的代价是每次失败都多一次检索、工具结果变长,不相关的经验还可能干扰模型。
4.8 动手实验 1:提示词改动到底改变了什么行为
从我项目真实的 runs.db 里,统计 v2 和 v3 两组运行中”手动传 toolchain 参数”的次数。v2 的提示词标准流程第 2 步是 cmake -B build -DCMAKE_TOOLCHAIN_FILE=.xbuild/zig.cmake [其他选项];v3 改成 cmake -B build [其他选项],并在环境一节加了”已经自动使用,不需要再传”。保存为 prompt_effect.py,在项目根目录运行。
import sqlite3
conn = sqlite3.connect("file:runs.db?mode=ro", uri=True)
GROUPS = {"v2(提示词让模型传 -DCMAKE_TOOLCHAIN_FILE)": (42, 101),
"v3(提示词说已由环境变量设置,不用传)": (102, 161)}
SQL = """
SELECT COUNT(DISTINCT r.id),
SUM(t.tool = 'run_command' AND t.args LIKE '%-DCMAKE_TOOLCHAIN_FILE%'),
COUNT(DISTINCT CASE WHEN t.tool = 'run_command' AND t.args LIKE '%-DCMAKE_TOOLCHAIN_FILE%' THEN r.id END),
SUM(t.tool = 'search_knowledge')
FROM runs r JOIN tool_calls t ON t.run_id = r.id
WHERE r.id BETWEEN ? AND ?
"""
for name, (lo, hi) in GROUPS.items():
runs, flag_calls, flag_runs, kb = conn.execute(SQL, (lo, hi)).fetchone()
print(f"{name}")
print(f" 运行 {runs} 次;手动传 toolchain 参数 {flag_calls} 次,出现在 {flag_runs} 次运行里;查知识库 {kb} 次")
实际输出(Python 3.14 自带的 sqlite3):
v2(提示词让模型传 -DCMAKE_TOOLCHAIN_FILE)
运行 60 次;手动传 toolchain 参数 113 次,出现在 60 次运行里;查知识库 10 次
v3(提示词说已由环境变量设置,不用传)
运行 60 次;手动传 toolchain 参数 1 次,出现在 1 次运行里;查知识库 13 次
逐段讲。
- run 编号范围怎么来的:v2 和 v3 各是 2 个配置 × 3 轮 × 10 个项目 = 60 次运行。我按
runs表的任务名和时间确认,v2 从 run 42(cJSON)到 run 101(re2),v3 从 run 102 到 run 161,run 162 开始是 LangGraph 版本。校验:两组查知识库的次数 10 和 13,和 REPORT 里 v2 rag、v3 rag 的”调用知识库”次数一致,说明范围划分是对的 JOIN tool_calls t ON t.run_id = r.id:把运行表和工具调用表按 run 编号连起来,每一行是一次工具调用SUM(t.tool = 'run_command' AND t.args LIKE '%...%'):SQLite 里比较表达式的结果是 1 或 0,求和就是满足条件的行数。LIKE '%X%'表示包含 X,%匹配任意字符COUNT(DISTINCT CASE WHEN ... THEN r.id END):CASE WHEN 条件 THEN 值 END在条件不满足时得到NULL,COUNT(DISTINCT ...)不计NULL,于是数出”至少出现过一次”的运行有几个WHERE r.id BETWEEN ? AND ?:?是占位符,参数用元组传入(14 篇)
看结果。
- v2 里 60 次运行全部手动传了参数,平均每次近 2 次。提示词的标准流程里写着这个参数,模型就照着做
- v3 里只剩 1 次。提示词说”不需要再传”,模型基本就不传了
- 这说明提示词里的具体命令模板对模型行为影响很大,模型会高度遵循”标准流程”里写出来的写法
但不能说”这是提示词单独的效果”。 v3 同时做了两件事:程序用环境变量设置了 toolchain,提示词也相应改了。模型不传参数是因为提示词说了不用传,但如果只改提示词、不设环境变量,模型不传参数的后果是编译用错编译器。提示词和环境必须一起改、保持一致,这也是 4.2 节 80cbff2 那次修复的教训。
真正能说明”效果”的是 REPORT 里的另一个数据:“子目录找不到 toolchain”从 8 次降到 0、路径检查误拦消失,这是这组改动要解决的问题,而且在轨迹里能直接对应到改动。
自己改一改:
- 把
LIKE条件换成%cmake -E rm%,看 v2、v3 里模型删除构建目录的次数(提示词里写了”要删除 build 目录用 cmake -E rm -rf build”) - 统计两组里
read_file调用中带offset参数的次数(提示:t.args LIKE '%offset%') - 给 SQL 加一个条件,只统计
status = 'success'的运行
4.9 根据失败案例迭代
flowchart TD
E[跑评测集] --> F[找出失败和异常的运行<br/>失败、步数异常多、工具失败率高]
F --> R[读轨迹<br/>模型在哪一步、为什么走错]
R --> C{原因归类}
C -->|不知道某个信息| P1[提示词补充环境或要点]
C -->|提示词和实际不一致| P2[修提示词或修代码<br/>让两边对齐]
C -->|知道但不遵守| P3[加强表述 给理由<br/>还不行就改机制]
C -->|工具设计有问题| P4[改工具<br/>返回格式 报错信息 分页]
C -->|评分器有问题| P5[修验收]
C -->|模型能力不够| P6[换模型 拆步骤 加工具]
P1 & P2 & P3 & P4 & P5 & P6 --> REG[回归评测<br/>确认问题解决 且其他没变差]
REG --> E
关键是先归类,再决定改哪里。 很多问题看起来是”模型没做对”,但原因不在提示词:
| 我项目的问题 | 表面现象 | 真实原因 | 改了哪里 |
|---|---|---|---|
模型反复写 head.cmake 分段读文件 | 绕路、浪费步数 | read_file 只返回末尾 100 行 | 工具(分页) |
| 模型在子目录找不到 toolchain | 配置失败 | 提示词让传相对路径参数,子目录下相对路径失效 | 环境变量 + 提示词 |
| libpng run 80 失败 | 步数用完 | 路径检查误拦了合法的相对路径 | 工具(环境变量后不再需要传) |
模型反复尝试 cat | 被拒 | 提示词和白名单不一致 | 提示词 |
| 知识库很少被查 | 调用率低 | 模型倾向于自己判断 | 提示词加强(效果有限),方向是改机制 |
| json 空构建判成功 | 通过率虚高 | 验收只看退出码 | 验收 |
六个问题里只有两个主要是改提示词解决的。 面试时如果被问”你怎么优化 Agent 效果”,只说”调提示词”是不够的。
防止过拟合评测集:如果每次都针对评测集里某个具体项目加一条规则(“编 libpng 时先编 zlib”),分数会涨,换个项目就没用了。判断标准是:这条规则能不能用一句通用的原因说清楚。“必需依赖先交叉编译再用 <Pkg>_ROOT 指过去”是通用的(这条后来整理进了知识库 k002),“编 libpng 先编 zlib”不是。另外 09 篇讲的留出集就是用来发现这种过拟合的。
4.10 动手实验 2:提示词版本管理
提示词改了之后,要能在运行记录和评测结果里知道用的是哪个版本。下面把提示词按段落组织,算内容哈希作为版本号,并生成两个版本的差异。保存为 prompt_version.py。
import difflib
import hashlib
SECTIONS_V1 = {
"role": "你的任务是把一个 C/C++ 项目交叉编译到目标平台。",
"tools": "search_knowledge:在交叉编译经验库里检索。编译或配置报错且你不确定怎么处理时调用它,把报错信息原文作为查询。不要在没有报错的时候调用。",
"done": "成功的判据是 cmake --build 返回 exit=0",
}
SECTIONS_V2 = dict(SECTIONS_V1)
SECTIONS_V2["tools"] = "search_knowledge:交叉编译经验库。每次 configure 或 build 失败后都先查一次,把报错的关键行作为查询,再决定怎么改。没有报错时不要调用。"
SECTIONS_V2["done"] = "成功的判据:cmake --build 返回 exit=0,并且构建目录里产出了目标架构的库、可执行文件或目标文件。"
def render(sections):
return "\n".join(f"<{name}>\n{text}\n</{name}>" for name, text in sections.items())
def version(text):
return hashlib.sha256(text.encode()).hexdigest()[:8]
p1, p2 = render(SECTIONS_V1), render(SECTIONS_V2)
print("v1", version(p1), len(p1), "字符")
print("v2", version(p2), len(p2), "字符")
for line in difflib.unified_diff(p1.splitlines(), p2.splitlines(), "prompt@" + version(p1), "prompt@" + version(p2), lineterm="", n=0):
print(line)
print("只改一个空格也会变版本:", version(p1 + " "))
实际输出(Python 3.14,只用标准库):
v1 5a533405 181 字符
v2 1c56e970 218 字符
--- prompt@5a533405
+++ prompt@1c56e970
@@ -5 +5 @@
-search_knowledge:在交叉编译经验库里检索。编译或配置报错且你不确定怎么处理时调用它,把报错信息原文作为查询。不要在没有报错的时候调用。
+search_knowledge:交叉编译经验库。每次 configure 或 build 失败后都先查一次,把报错的关键行作为查询,再决定怎么改。没有报错时不要调用。
@@ -8 +8 @@
-成功的判据是 cmake --build 返回 exit=0
+成功的判据:cmake --build 返回 exit=0,并且构建目录里产出了目标架构的库、可执行文件或目标文件。
最后一行输出是 只改一个空格也会变版本: b23c7871。
逐段讲。
- 两个版本里的文字取自项目 git 历史中真实的前后两版(做了简化,把工具描述和成功判据放进同一个示例里)
dict(SECTIONS_V1):复制一份字典。直接写SECTIONS_V2 = SECTIONS_V1只是让两个名字指向同一个字典,改 V2 会把 V1 也改掉render:把每一段包进同名的 XML 标签,按顺序拼起来。Python 3.7 起字典会保持插入顺序hashlib.sha256(text.encode()).hexdigest()[:8]:encode()把字符串转成字节;SHA-256 是一种哈希算法,内容有任何变化结果都会完全不同;取前 8 位作为简短的版本号difflib.unified_diff(旧行列表, 新行列表, 旧名, 新名, lineterm="", n=0):生成和git diff格式相同的差异。-开头是删掉的行,+开头是新增的行;n=0表示不显示上下文行;lineterm=""让每行末尾不再额外加换行
看结果。
- 两个版本的哈希不同,可以直接写进运行记录(比如
runs表加一列prompt_version) - 差异清楚地显示出只改了工具描述和成功判据两段
- 加一个空格哈希就变了。这是好事:任何改动都能被发现;但也意味着格式整理这类无关紧要的改动也会产生新版本,所以版本号用来”识别”,改动是否重要要看差异
实际项目里怎么落地:
| 做法 | 说明 |
|---|---|
| 提示词和代码一起进 git | 改动走代码审查,能看到谁、什么时候、为什么改 |
| 运行记录里存版本号 | 10 篇的链路追踪里可以作为运行级别的属性 |
| 评测结果文件里存版本号 | 结果变化时能确认是不是提示词变了 |
| 改提示词必须跑回归评测 | 09 篇的回归关卡 |
| 线上灰度 | 新版本提示词先给一小部分流量,对比指标 |
我项目的缺口:评测结果里记了模型、是否开知识库、步数上限、被测项目的 commit,但没有记录提示词版本。v2、v3 用的是哪版提示词,只能靠 git 提交时间和报告日期对应。实验 1 里 run 编号范围也是靠任务名和时间人工确认的。
自己改一改:
- 在
SECTIONS_V2里新增一段"limits",看差异输出 - 把
n=0改成n=1,看上下文行 - 写一个函数,只对去掉首尾空白、合并连续空格后的文本算哈希,让格式整理不产生新版本。想一想这样做的风险
4.11 提示词做不到的事
| 需求 | 为什么提示词不够 | 该用什么 | 我项目 |
|---|---|---|---|
| 安全边界 | 模型可能不遵守,还可能被注入 | 工具层检查、沙箱(11 篇) | 提示词说只能访问工作目录,模型照样 ls /opt/homebrew,是代码拦下的 |
| 输出格式 | 模型偶尔不按格式输出 | 结构化输出、工具调用、程序校验(02 篇) | 以工具调用为主,参数 JSON 解析失败时回给模型 |
| 判断任务完成 | 模型会说”成功了”但实际没有 | 程序化验收(05、09 篇) | verify 独立检查 ELF 产物 |
| 稳定地做某件事 | 写得再强调,模型也可能不做 | 用代码让它必然发生 | 知识库调用率低,方向是自动检索 |
| 限制步数和成本 | 模型不知道自己花了多少 | 代码里的步数、token、时间上限(05、10 篇) | 步数上限 40 |
| 保密 | 提示词可能被套出来 | 不把机密放进提示词(11 篇) | 提示词里没有密钥 |
提示词在这些地方的角色是”让模型知道”,而不是”保证发生”:提示词里写成功判据,是让模型知道目标,减少它提前宣布完成;写命令限制,是让模型少浪费步数去撞墙。真正的保证在代码里。
提示词注入:工具结果、检索文档、用户输入里的文字都可能包含”忽略之前的指令”这类内容。4.5 节的标签分隔能降低风险,但根本的防护是权限最小化和避免”致命三要素”,11 篇详细讲。
第五部分 对照项目
| 本篇知识点 | 项目里的位置 | 做到了什么 | 没做到或可以改进的 |
|---|---|---|---|
| 系统提示词结构 | build_agent.py 的 SYSTEM_PROMPT | 环境、流程、限制、要点、成功判据、输出要求分段 | 没用 XML 标签分段;没有版本标识 |
| 给理由 | ”主机上的库和头文件不能用于交叉编译”、“写了也不会生效” | 模型知道原因 | — |
| 说该做什么 | 每条”没有 X”都跟”用 Y” | 减少绕路 | — |
| 和环境一致 | 提交 80cbff2 对齐命令清单 | 修正了 cat 不一致 | 提示词”失败两次换思路”和代码”超过 3 次拦截”不一致 |
| 根据失败迭代 | 要点里 -Werror、header-only 等条目 | 多条有 git 历史可查 | 部分条目的先后因果从记录里没法完全确认 |
| 工具描述 | KB_SCHEMA | 从”不确定时”改成”每次失败都先查”,调用 3 → 5 | 调用率仍低;该轮三处同时改 |
| 行为验证 | runs.db 统计 | v2 → v3 手动传 toolchain 参数 113 次 → 1 次 | 需要人工确认 run 编号范围 |
| 示例 | — | 有意不用,改用启发式要点和知识库 | 分类类子任务可以用示例 |
| 版本管理 | git | 提示词在代码里,改动可追溯 | 运行记录和评测结果里没有提示词版本 |
| 提示词和代码分工 | 工具层检查、verify、步数上限 | 保证类需求都由代码实现 | — |
第六部分 追问清单
| 你刚讲完 | 下一个追问 | 回答方向 |
|---|---|---|
| 系统提示词结构 | 为什么不写”你是资深专家” | 角色设定作用有限;具体的环境、流程、限制更有用 |
| 合适的高度 | 怎么判断写得太死 | 在写具体 if-else、针对具体项目的规则;换个场景就失效 |
| 给理由 | 为什么有用 | 模型能从原因推广到没写到的情况 |
| 示例 | 给几个 | 3 到 5 个,相关、多样、用标签包起来 |
| 示例 | 你项目为什么没用 | 项目差异大,示例会被照搬;用要点和知识库替代 |
| 标签 | 能防注入吗 | 能降低,不能保证 |
| 长文档 | 放哪里 | 放前面,问题放后面;和缓存的”稳定前缀在前”不冲突 |
| 工具描述 | 改描述有用吗 | 3 → 5,有效但有限;越写越重还不做就改机制 |
| 行为验证 | 怎么证明提示词改动有效 | 定义要改变的行为,从运行记录统计;v2 → v3 113 → 1 |
| 因果 | 113 → 1 是提示词的功劳吗 | 和环境变量一起改的;真正要解决的问题是找不到 toolchain 8 → 0 |
| 迭代 | 效果不好先改提示词吗 | 先归类原因;我项目六个问题只有两个主要靠改提示词 |
| 过拟合 | 怎么避免针对评测集打补丁 | 规则能否用通用原因说清;留出集检验 |
| 版本管理 | 怎么做 | 进 git、哈希版本号写进运行记录和结果、回归评测、灰度 |
| 分工 | 提示词做不到什么 | 安全、格式保证、验收、成本上限 |
| 不一致 | 提示词和代码不一致会怎样 | 模型反复撞墙;cat 的例子 |
第七部分 闭卷自测
1. 除了系统提示词,Agent 里还有哪些地方的文字会影响模型行为?各举一个我项目的例子。
答案
工具名和描述(知识库工具描述改写)、参数描述(timeout 超时秒数)、用户消息(任务 + 项目探测结果)、工具结果(分页提示”用 offset=101 继续读”)、报错信息(越界时说明主机库不能用于交叉编译)、历史。
2. Anthropic 说的系统提示词两个极端是什么?用交叉编译的例子各说一个。
答案
太死:把复杂脆弱的逻辑硬编码,比如写一张”报错包含 X 就执行 Y 命令”的查表,措辞一变就失效。太虚:空泛指导没有具体信号,比如”你是交叉编译专家,请认真分析报错”。合适的是给原因和几个方向的启发,比如 -Werror 那条要点。
3. 为什么给指令加上理由效果更好?把”不要访问工作目录外的路径”改写成带理由的版本。
答案
模型能从理由推广到没写到的情况。改写:“只能访问工作目录内的路径,主机上的库和头文件属于本机平台,不能用于交叉编译。“这样模型明白”去主机上找依赖”这个方向本身就是错的。
4. 我项目系统提示词的”命令限制”一段有什么值得学的写法?
答案
每条”没有 X”都给出替代方案(没有 rm 用 cmake -E rm -rf,没有 cat 用 read_file);说明后果(“写了也不会生效”);和工具层实际白名单保持一致;具体到参数值(configure 传 180、build 传 300)。
5. 提交 80cbff2 修的是什么问题?说明了什么原则?
答案
cat 已从命令白名单删除,但提示词还说能用,模型会反复尝试被拒;同时把”主机的库不能用于交叉编译”提前写进提示词。原则:提示词必须和代码实际行为一致;能提前告知的信息不要等模型撞墙后从拒绝信息里学。
6. few-shot 示例有哪些要求?为什么示例的答案不能都一样?
答案
相关(接近实际场景)、多样(差异足够大)、用 <example> 标签和指令分开、3 到 5 个。答案都一样模型可能学到”总是回答这个”的错误规律;输入长度、类型也要多样。
7. 知识库工具描述改写前后有什么区别?结果如何?这说明了什么?
答案
从”不确定怎么处理时调用”改成”每次 configure 或 build 失败后都先查一次”,并说明能省掉大量试错、查询用报错关键行。调用从 3 次变成 5 次,但两百多次工具调用里仍然很少,且该轮三处同时改。说明提示词引导有效但有限;越写越重还不做时,应改成代码机制(失败后自动检索)。
8. 实验 1 里 v2 到 v3 手动传 toolchain 参数从 113 次降到 1 次,能说”提示词改动让效果变好了”吗?
答案
不能这么说。它说明模型高度遵循提示词里写出的命令模板;但 v3 同时用环境变量设置了 toolchain,提示词和环境是一起改的。这组改动真正要解决的问题是”子目录找不到 toolchain”,从 8 次降到 0、误拦消失,这才是效果,并且能在轨迹里直接对应到改动。
9. 实验 1 的 run 编号范围是怎么确认的?怎么验证划分是对的?
答案
v2、v3 各 60 次(2 配置 × 3 轮 × 10 项目),按任务名和时间确认 v2 是 run 42 到 101、v3 是 102 到 161,162 起是 LangGraph。验证:两组查知识库次数 10 和 13 与 REPORT 里 v2 rag、v3 rag 的调用次数一致。
10. 效果不好时为什么不能直接改提示词?我项目六个问题里有几个主要靠改提示词解决?
答案
原因可能在工具设计、提示词和环境不一致、评分器、模型能力,改提示词解决不了。六个问题里主要靠改提示词的是 cat 不一致和知识库调用引导两个(后者效果有限);read_file 绕路、子目录 toolchain、误拦、空构建判成功分别改了工具、环境、验收。
11. 怎么判断一条新加的提示词规则是在”改进方法”还是在”针对评测集打补丁”?
答案
看这条规则能不能用一句通用的原因说清楚。“必需依赖先交叉编译再用 <Pkg>_ROOT 指过去”是通用的;“编 libpng 时先编 zlib”是针对具体项目的。再用留出集检验能否推广。
12. 实验 2 里为什么用内容哈希做版本号?有什么副作用?我项目在版本管理上缺什么?
答案
内容任何改动哈希都会变,能自动识别版本、写进运行记录和评测结果。副作用是加个空格这种无关改动也产生新版本,是否重要要看差异。我项目评测结果里没有记录提示词版本,只能靠 git 时间和报告日期对应,run 范围也要人工确认。
延伸阅读
- Anthropic:Effective context engineering for AI agents(2025-09)— 合适的高度、分段、最少信息、从失败模式迭代、典型示例
- Anthropic:Prompting best practices — 写清楚、给理由、示例、XML 标签、长文档位置
- Anthropic:Prompt engineering overview — 先定义成功标准和评测方法再做提示词工程
- Anthropic:Building effective agents(2024-12)— 附录里的工具提示词设计
下一篇:04 工具调用与工具设计——提示词告诉模型怎么想,工具决定模型能做什么。