应用系统体系架构-01:Prompt 工程
大模型应用需要把自然语言任务转化为可检查的输入、输出和执行过程。Prompt 工程 负责描述任务与约束,上下文工程 负责选择和组织模型能够看到的信息;二者共同影响模型的判断,但系统可靠性还需要类型校验、工具执行和测试反馈。
以代码审查为例,模型需要知道审查哪些文件、使用哪些规则、项目依赖什么版本,以及如何表示发现的问题。缺少这些信息时,即使回答流畅,也可能引用不存在的 API、误判代码行为,或者生成下游程序无法读取的结果。
课程定位
本系列八章对应整门课程的 AI 应用单元。课程整体还包括服务端开发、数据库与分布式系统:服务端关注状态、并发、通信、事务和缓存;数据库部分关注存储、索引、恢复与不同数据模型;分布式部分关注容器、集群和批流处理。AI 助手最终仍要运行在这些应用基础设施上。
电子书店贯穿这些内容。例如“推荐算法相关的书并购买”需要先查当前商品与库存,再读取促销规则,最后按权限处理订单。Prompt 描述目标,MCP/Tool 取得或修改真实数据,RAG 提供私有资料,Skill 组织复用方法,Agent 协调步骤,Graph 与 Harness 控制执行和验证。
AI 生成的代码也需要明确架构要求。只要求“做一个书店前端”,可能得到功能集中在单个 JavaScript 文件中的实现;若要求区分页面、组件、接口访问和状态管理,还需检查这些职责是否真的分开。能运行是验收的一部分,理解生成代码的依赖、状态和修改范围同样重要。
生成机制
自回归语言模型根据已有 Token 预测下一个 Token。对于序列 ,其概率可以分解为:
生成的早期内容会成为后续生成的条件。若模型先写错了一个方法名,后面可能继续沿用该名称,并围绕它生成相关类和调用。这种现象称为 错误级联。例如把真实方法 getUserInformation() 写成 getUserInfomation(),会使调用、测试和类型定义一起偏离项目实际接口。
Transformer 使用注意力建立上下文中不同位置的关联。缩放点积注意力为:
、、 分别是查询、键和值, 是键向量的维数。注意力帮助模型关联变量声明、函数定义和引用,但不等同于编译器的符号解析。模型仍可能忽略远处的约束,或者把文本中一个合理的名称误当成真实接口。
生成过程本身没有“编译通过”的保证。应用应把真实接口、类型和测试结果作为可核验条件,并允许后续修改已经生成的内容。
温度控制对候选 Token 的采样。设模型输出 logits 为 ,在 时可以表示为:
较小的 使高分候选更占优势,较大的 使分布更平缓。 表示使用原来的 softmax 分布,不表示从全部 Token 均匀随机选择; 在接口中通常表示贪心选择或相应近似规则,而不是直接代入上式。
这可以与“利用已有较好方案”和“探索其他方案”类比:总选同一家已知餐馆比较稳定,尝试其他餐馆则可能找到新选择。采样增加多样性,但不会保证找到全局最优,也不能单靠温度消除事实错误。代码验证通常偏重稳定,多候选搜索可以适度增加多样性。
同一个问题在已有对话中再次提出时,上下文也已经变化,因此回答变化未必只由温度造成。temperature=0 仍可能产生错误,也不保证在不同服务实现和运行环境中获得逐字相同的结果。
闭式计算与迭代方法可以帮助理解这种权衡。例如已知方程形式时可以直接代入求根公式,Newton 方法则用 逐步修正近似解,结果受函数与初值等条件影响。模型训练也常使用迭代优化,但推理时调整 temperature 只是改变采样分布,不会重新训练权重,也不等同于对损失函数执行一次跳出局部最优的优化。
Prompt 契约
一个完整的 Prompt 应包含 角色、任务、约束。角色限定职责和分析视角,任务描述操作对象与目标,约束规定允许的修改、输出形式和失败行为。
| 要素 | 代码审查中的含义 | 可检查的表达 |
|---|---|---|
| 角色 | 以安全和性能为审查范围 | 检查 SQL 注入、资源泄漏、并发问题 |
| 任务 | 分析给定文件或差异 | 逐项给出规则编号、位置和修复建议 |
| 约束 | 限制结论与输出 | 只报告有代码证据的问题,返回约定 JSON |
“代码要优雅”“性能要很好”等要求缺少验收标准。可以改成“不改变公开方法签名”“避免在循环中执行重复查询”“圈复杂度不超过约定阈值”等具体条件。角色描述只能引导模型,不能赋予数据库、文件或外部服务的访问权。
角色与 Scope 还决定模型应优先考虑哪些知识和规则,例如 Java 代码审查要说明关注并发、安全还是界面行为。角色是输入条件,不会改变已经训练好的权重,也不能保证直接选中某个固定的“数学专家”。
MoE(Mixture of Experts) 把部分计算交给 Router 选择的专家子网络,通常根据 Token 表示激活有限专家以控制计算量。它与按任务选择资料有相似的直观想法,但专家不是必须按“数学、中文、计算机”人工划区,稠密模型也可以遵循角色要求。Attention 与 FFN 的具体开销随模型结构、输入长度和硬件变化。
防御性设计将任务契约分成三个环节:
- 前置检查:确认代码、依赖、运行环境和必要业务信息齐全。
- 后置校验:检查输出结构、类型、目标路径和业务规则。
- 失败处理:信息不足时返回明确状态,列出缺失信息及仍可判断的内容。
这些检查既应写入任务说明,也应尽可能由宿主程序执行。例如空代码输入可以直接拒绝,无需先消耗一次模型调用;路径越界应由程序拦截,无需等待模型自我判断。
任务模板
下面把角色、任务和边界组织成一个代码审查模板。待审查代码与可信任务要求使用不同区块。
1 | <review_task> |
文件路径、语言和版本信息让模型能够区分不同文件及语法环境。把任务拆成“理解输入、检查规则、生成结果、验证边界”的步骤,还能明确失败发生在哪个环节。
慢 SQL 分析适合说明前置条件的重要性。下面的查询使用 DATE() 包裹时间列:
1 | SELECT * |
可以提出以下日期范围改写:
1 | WHERE o.created_at >= '2026-08-01 00:00:00' |
这种改写可能让普通索引更容易参与过滤,但最终效果取决于数据库、列类型、时区、函数索引、数据分布和执行计划。数据库类型、已有索引和数据规模缺失时,应指出候选改写及验证方法,不能直接宣称某个复合索引必然最优。
SQL 描述要取得什么结果,执行器再选择连接顺序、扫描方式和索引。同一语句在不同数据库或版本中可能有不同计划,书写 orders JOIN users 也不等于物理上必然先扫描 orders。因此,优化任务应提供数据库与版本、表结构、已有索引、数据分布和 EXPLAIN 结果;缺少这些内容时,应返回待补信息和有限建议。
上下文组织
上下文通常包含系统要求、项目规则、当前任务、示例、检索材料和历史消息。组织上下文的目标是让模型清楚地区分 必须遵守的任务要求 和 需要分析的资料。
| 格式 | 适用内容 | 需要处理的问题 |
|---|---|---|
| Markdown | 文档、代码片段、标题与列表 | 围栏嵌套、层级过多、边界不明确 |
| XML 标签 | 多文件内容、角色与资料区分 | 特殊字符转义、伪造闭合标签 |
| YAML | 配置和规则元数据 | 缩进、类型解释、多行字符串 |
| JSON | 程序接口和结果契约 | 引号、转义、严格结构与 Schema |
Markdown 可以通过更长的外层围栏包含内部三反引号;XML 可以用 path 和 language 属性表达文件信息;YAML 适合表示参数和规则。格式选择应依据输入内容和解析方式,而不是假设某一种格式天然安全。
输入组织与输出序列化是两个独立决定。程序可以用 XML 包装多文件输入,同时要求模型返回 JSON;普通聊天界面则不必让用户手写 XML。YAML 的空白与缩进是正式语法的一部分,需要正确解析;不能从它更省字符或人看起来更简洁,推出机器无法可靠处理。
多文件任务还需要区分依赖关系。例如先提供 Order 的类型、公开接口和调用者,再提供待修改的 OrderService,通常比无序粘贴全部文件更容易理解。检索时可以保留 AST 中的类、方法签名与相关调用,省略与当前目标无关的实现。
注意力分布
上下文窗口限制可输入的 Token 数,但窗口较大不意味着模型能同等有效地利用每个位置的信息。Lost in the Middle 指部分长上下文任务中,相关信息放在中间位置时,模型表现比放在开头或结尾更差。

图示用于说明位置效应,其中比例不应解释为所有模型固定不变的分界。设计上下文时可以在开头说明核心约束,在任务末尾简要重申最重要的验收条件;检索材料则按相关性组织。重复要求应保持简短,避免与原规则冲突。
对于长历史,应区分近期工作信息和已经确认的长期事实。把所有旧方案、失败补丁和无关日志持续加入上下文,会增加噪声,并挤占当前任务需要的资料。
前缀缓存
自回归生成中,已经处理的 Token 在各层产生 K、V 表示。KV Cache 保存这些中间计算,后续生成可以复用,不必重新计算已有前缀。它保存的是模型内部张量,不是向量知识库,也不是在 Prompt 后追加的一段文本。
支持跨请求前缀复用的推理服务,还可以复用多个请求共有的前部内容。因此,反复执行同类任务时,可以把稳定的角色、规则、工具说明放在前面,把每次变化的问题和文件放在后面。
1 | 固定前缀:代码审查角色、允许规则、输出 Schema。 |
前缀缓存依赖实际 Token 序列及相应模型配置,不能把“意思相近”直接当成命中。随意改写固定模板、打乱工具顺序或在开头插入变化字段,都可能降低可复用长度。能否跨请求缓存、保存多久以及如何计费由具体服务决定。命中缓存的前缀仍属于逻辑输入,减少重复计算不等于扩大模型的上下文窗口。
缓存中间计算不等于缓存最终答案。共用审查规则的两个任务仍需分别检查各自代码;相同库存问题也可能因业务状态变化得到不同结果。
窗口管理
一次请求的预算需要同时覆盖System Prompt、工具定义、历史、检索文本和预留输出:
剪枝 删除无关内容,压缩 把较长资料转成保留关键事实的摘要,滑动窗口 控制近期对话的长度。三者可以结合,但摘要应保留目标、限制、已确认事实、失败原因和后续行动,不能只留下“已讨论过某问题”这样的概括。
上下文污染发生在错误内容被后续轮次当作事实使用时。处理方式是标明错误、替换已过时结果,并保留修正依据。必要时重置会话或重新装配上下文;不必在每次失败后丢弃全部有效历史。完整日志和原始资料应保存在可检索位置,以便摘要不够时回溯。
界面里看到的最后一句问题通常不是完整 API 输入。宿主还可能附加 System Prompt、工具 Schema、检索片段、历史工具结果与摘要,预算需要以实际发送内容为准。只数用户输入字数,无法解释整个请求的 Token 用量。
三种缩减方式也有不同语义:AST 剪枝可以保留方法签名与说明、暂时省略方法体;摘要用新的短文本概括较长内容;滑动窗口保留限定轮次或时间范围内的原记录。需要分析具体实现时,只有签名通常不够,应重新读取相关方法体。摘要也可能失真,不能当成未经改动的原文。
结构化输出
自由文本适合解释,但程序接口通常需要明确字段。代码审查结果应包含文件、行号、严重程度、规则编号、建议和可选补丁;PR 总结应包含变更范围、行为影响、风险和实际测试记录。
结构化输出可以分为几种约束强度:
| 方式 | 实际作用 | 局限 |
|---|---|---|
| Prompt 要求 JSON | 引导输出格式 | 仍可能夹带说明或围栏 |
| Few-Shot 示例 | 示范字段与取值 | 示例错误可能被模仿 |
| JSON Schema | 描述类型与结构 | 需要确认生成接口支持的子集 |
| 受限解码 | 在生成时限制合法形式 | 不保证字段内容正确 |
| 运行时校验 | 收到结果后检查契约 | 失败时仍需修正或终止 |
GBNF 等文法可以描述允许生成的结构,语法状态与 Token 屏蔽共同约束输出。Pydantic 校验则发生在结果返回之后,两者作用阶段不同。
json.loads() 只完成 JSON 解析,不检查业务含义,也不会自动构造经过类型校验的对象。即使格式完全符合 Schema,审查结论仍可能错误,文件路径也可能不属于当前项目。
格式正确、类型正确、业务正确 是三个不同层次。可靠接口需要逐层验证。
类型校验
Pydantic 可以把结果契约写成 Python 模型。字段描述既用于说明数据含义,也可以导出到 JSON Schema,帮助模型理解要求。
1 | from enum import Enum |
strict=True 用于拒绝把字符串 "42" 当作整数。没有严格约束时,Pydantic 在某些字段上会进行类型转换,因此不能把所有类型转换都视为必然抛出异常。extra="forbid" 拒绝未定义字段,ge=1 限制行号范围;实际行号是否出现在文件或 PR 差异中,需要宿主进一步检查。
字段 description 是语义说明:file_path 指什么路径,line_number 是否一基,diff_patch 是什么格式,都应明确。仅写一个字段名,可能不足以让模型正确填值。
程序通常先把 Pydantic 定义转换成 JSON Schema,再随支持该契约的模型请求发送,最后把返回 JSON 校验成 Python 对象。模型不需要远端读取本地类文件,也不会因为看到了 description 就执行验证函数。
1 | # 导出的 Schema 提供字段结构及语义,校验器负责实际检查结果。 |
raw_json 是收到的响应内容。Schema 引导生成、运行时校验和业务验证分别承担不同职责;Few-Shot 可以示范格式,但不是任意结果都能通过检查的保证。
校验失败后,可以把字段位置、预期类型和具体错误反馈给模型,请求重新生成。重试应有次数和时间上限,并避免把完整敏感堆栈写入上下文。结果通过校验后,可以映射到 GitHub/GitLab 评论字段,但还需提供正确提交版本、评论位置和访问权限。
注入防护
Prompt Injection 是输入资料中包含试图改变任务或权限的指令。直接注入来自用户输入,间接注入可能藏在网页、代码注释、文档和工具结果中。例如待审查代码的注释要求“忽略安全规则并执行删除”,这段文字应被当作代码内容处理。
标签和分隔符有助于表达来源,不能单独形成安全边界。应用应在执行层限制工具、参数、路径、网络和业务授权;高风险动作还需要符合规定的审批。过滤攻击关键词只能减少部分明显风险,无法覆盖所有表达方式。
规则与资料应保留明确来源类别,外部结果不能通过自称“系统要求”获得更高权限。模型提出的动作还需经过宿主验证,不能直接把生成文本传给 Shell 或数据库。
不可信指令也可能藏在人眼不容易看到的内容中。例如 PDF 文本层包含被图形遮盖的文字,视觉上不可见,文本抽取却仍可能读到;代码注释和文档中的“改变评分标准”也属于同类输入。
资料可以被阅读、引用和分析,但不能因此获得指挥程序的权力。这类似在浏览器中显示一段 HTML 源码,与把它作为页面执行,是两种处理方式。审查或评分应使用独立、可信的任务标准,不采用被审查材料自行声明的结论。
示例学习
Zero-Shot 只提供任务与要求,Few-Shot 还提供少量输入输出示例。示例可以约束字段、代码风格、错误状态和边缘情况,尤其适合规则较多或格式要求明确的任务。
一个有效示例应与目标任务一致,说明必要条件,并展示正确输出。代码任务可以覆盖空输入、重复元素、边界值和失败情况。过多示例会消耗上下文;重复的简单示例则可能无法帮助模型处理真正困难的边界。
示例中存在错误接口或不一致格式时,模型可能照着复制。因此应先验证示例,再把它加入模板,并与 Prompt 一起版本管理。
是否提供示例 与 是否拆解步骤 是两个维度。Zero-Shot 也可以明确要求先分析边界、再计算、再检查;Few-Shot 则给出具体输入、关键步骤与输出供参考。
例如“3 人 3 天喝 3 桶水,9 人 9 天喝多少”需要先明确假设:每人每天消耗相同。单位消耗为 桶/人/天,总量为 桶。示例应展示这个数量关系,而不只让模型模仿“3 对应 9”的数字替换。人数、书本数和公交站数也不能未经语义判断直接相加。
推理方法
Chain-of-Thought(CoT) 通过中间推导组织复杂问题。若 是问题、 是中间步骤、 是答案,可以把答案概率写为:
Zero-Shot CoT 通过“逐步分析”等要求引导分解;Few-Shot CoT 提供问题、关键推导与答案的示例。代码任务中,关键步骤可以包括识别约束、选择数据结构、分析复杂度和验证边界。
例如寻找两个数之和时,可以先解释哈希表保存已见值,再判断当前值的补数是否存在,并说明重复元素和无解情况。这样的推导比直接给代码更容易审核,但推导文本仍可能有错。
应用通常需要 可核验的关键步骤与结果,无需依赖模型完整的内部推理。对于已有推理能力的模型,应给出明确目标和验证要求,再用运行结果检查答案。
多路径搜索
Self-Consistency 对同一问题生成多个候选,再聚合答案。算术任务可以对归一化数值投票;代码任务更适合按测试表现筛选,因为不同代码文本可能具有相同行为,同样的代码结构也可能包含共同错误。
奇数候选可以减少二元投票的平票机会,但不保证一定形成多数:三个候选也可能分别给出三个不同答案。聚合规则需要明确如何归一化答案、处理并列与无法验证的结果;不能把“一致”自动解释成“正确”。
Least-to-Most 将复杂问题拆成逐步依赖的子问题,先求解较简单部分,再把结果用于后续步骤。每个子问题应有输入、输出和验收条件,防止前一步错误持续传播。
Tree-of-Thoughts(ToT) 把候选中间步骤组织成搜索树,通过扩展、评估、剪枝和回退探索不同路线。宽度、深度和评分器决定成本;代码搜索可以用编译和测试评分,模型自评只作为辅助。
任务分解树与候选搜索树需要区分。把任务拆成读取、修改和测试等子任务时,可能必须完成所有依赖步骤;探索几个不同解法时,则可以只保留通过验证的一条路线。到达叶子仅表示某条路径结束,是否成功仍由验收条件判断。
| 方法 | 主要用途 | 额外成本 |
|---|---|---|
| CoT | 梳理单条推导路线 | 更长输出 |
| Self-Consistency | 比较多个答案候选 | 多次生成与聚合 |
| Least-to-Most | 处理子问题依赖 | 多阶段调用与验证 |
| ToT | 搜索并回退多个方向 | 分支展开与评分 |
困难算法题可以比较这些方法,但必须固定模型、题集、采样数、测试环境和判分规则。没有这些条件的胜率只能帮助理解趋势,不能用于判断某方法在所有代码任务中更好。
反馈修订
Self-Refinement 由生成、检查和修订组成。检查可以来自模型评论,也可以来自编译器、静态分析、测试和真实运行结果。外部验证更容易提供可复现证据,例如指出某个测试输入对应错误输出。
修订应针对具体失败条件。空列表测试失败时,应修正边界处理;接口兼容测试失败时,应恢复公开签名。每轮还要检查原本通过的任务,避免修复一个问题后引入其他回归。
当重复尝试没有获得新证据,或者相同错误持续出现时,应停止、重新获取信息或转交处理。无界自我修改会消耗预算,并可能让代码逐渐偏离原目标。
工具闭环
纯文本推理不能确认当前文件内容、数据库状态或测试结果。ReAct 将任务分析、动作和观察交替进行,让真实工具结果影响下一步决策。

典型 Bug 修复流程是先读取代码并复现故障,再定位原因、生成最小补丁、执行测试,最后判断是否完成。模型负责提出候选动作,控制器负责验证参数、执行工具和回传结果。
1 | 目标:修复登录接口的空值异常。 |
控制器需要保留调用和结果的对应关系。原生 Function Calling 使用工具名称、参数和调用 ID 表达动作,文本 ReAct 则常使用显式区块或结构化对象。两者属于协议与控制方式的不同层次,可以共同使用。
循环控制
工具循环需要成功、失败、等待审批和预算耗尽等出口。重复动作可以通过工具名与规范化参数的指纹检测,但还应判断观察是否变化;合法轮询可能反复请求同一状态。
错误应区分参数错误、运行故障和业务失败。缺少参数可以反馈字段错误,临时网络故障可以有限重试,权限拒绝不能通过改写提示绕过。测试日志需要保留失败名称、堆栈关键位置、退出码和相关上下文,并给完整日志留出查询入口。
下面用控制流程表达一个最小编码助手。工具执行器、模型客户端和验证器属于明确的接口,不能把未实现的占位函数当作完整系统。
1 | 接收任务并检查上下文。 |
项目规则
Cursor、GitHub Copilot、Cline 等 AI 编程工具会组合不同上下文来源。FIM 补全关注光标前后的代码,项目检索提供相关定义和文件,AST 与依赖分析帮助理解影响范围,工具循环则连接终端、测试和修改行为。
规则应按组织、项目、路径和当前任务分层加载。公共规范规定安全与代码风格,局部规则说明模块接口和业务不变量。规则文件需要版本管理,避免多个文件重复或冲突,并在修改后运行回归评测。
重构任务可以固定为“读取依赖、建立测试基线、生成受限补丁、验证行为”四个阶段。约束应包括公开 API 兼容、依赖限制、目标目录、补丁规模和失败处理。AST 分析还需与运行时信息结合,因为反射、配置和动态调用可能不出现在普通静态调用图中。
PR 总结生成器同样适合这一结构:先读取差异,再识别行为变化,最后生成结构化摘要。测试字段只能来自执行记录;未运行的测试应说明未运行,不能填成通过。
Prompt 评测
Prompt 优化需要固定评测集。Pass@1 表示一次候选成功的比例,Pass@k 表示 个候选中至少一个成功的情况。若一次生成 个候选,其中 个正确,常用估计为:
函数级任务可以使用 HumanEval 一类测试,仓库级修复可以使用 SWE-bench 一类任务;二者的上下文、执行环境和验收不同。评测应覆盖正常、边缘和历史失败任务,并同时记录调用次数、延迟、成本和失败类型。LLM-as-a-Judge 可以辅助判断解释质量,编译、测试和业务断言则负责可重复的硬验证。
Prompt、示例、规则和模型配置应一起版本管理。修改后比较新旧结果,通过 CI 检查关键场景退化;线上 Trace 和失败日志可以补充后续评测集。
可靠的 Prompt 系统应让任务边界可表达、结果可校验、动作可控制、失败可追踪。Prompt、上下文、推理方法与工具闭环共同服务于这些要求。

