生成式软件工程
搜索
开始阅读/第 1 部分 · 机器开始会干活/第 2 章

Agent 由什么组成

一个能用的小助手,核心只有两百行。真实产品在这两百行外面,套了八层东西。

这一页讲什么
  • 一个模型、一个循环、八层外壳,八层各自解决什么麻烦、付出什么代价;
  • 为什么调外壳比换一个更强的模型更划算,发动机与整车这个类比怎么用;
  • 工具描述本身就是提示,写坏一句会出什么问题;
  • 工具接得越多,注意力越分散,背后的数字怎么算;
  • 在自己电脑上就能做的两个观察实验。

一、一个循环,加上八层外壳

先看清最小的那个东西。一个能读文件、能改文件、能回答你问题的小助手,骨架只有四件事:

  1. 在窗口里读一行输入,把这句话追加进对话记录;
  2. 把对话记录连同一份工具清单发给模型;
  3. 模型说要调用某个工具,就在本地执行那个工具;
  4. 把执行结果追加回对话记录,回到第 2 步。

四步循环起来,它就能干活了。这个骨架两百行以内写得完,至今没有变过。这一章要讲的每一件事,都发生在骨架外面。

flowchart LR
  U["你输入一句话"] --> H["追加进对话记录"]
  H --> M["连同工具清单发给模型"]
  M --> D{"模型要调用工具吗"}
  D -->|"要"| T["本地执行那个工具"]
  T --> R["结果追加回对话记录"]
  R --> M
  D -->|"不要"| A["给出回答"]

工业级产品做的是同一件事,差别在规模。同样是这一个循环,工具从 3 个涨到 40 个左右,外面多出八层设计。八层合起来的正式名字叫Harness(外壳)。这个词本来指马具,套在马身上、让马的力气能被有效使用的整套装备。放到这里,它指包在大语言模型外面的那层代码与配置,负责告诉模型它是谁、能调用什么、哪些信息进上下文、什么时候自动跑检查、压缩历史时保留什么。

八层总表:每一层解决什么麻烦,代价是什么

先把八层一次列全。第三列是各自解决的麻烦,第四列是付出的代价。代价那一列新手最容易忽略,它决定了这八层不能无限地加。

序号部件解决什么麻烦主要代价
1系统提示模型不知道自己在哪、该怎样、不许做什么写得太长会挤占上下文预算
2配置文件每个项目的规范不同,没法固定在系统提示里容易写成一本手册,臃肿
3工具列表没有工具,助手只是一个聊天窗口工具越多,选错的机会越多
4计划模式方向错了还改了几十个文件,返工代价巨大简单任务上多花一步
5技能复杂流程每次都要重新讲一遍写不好等于没写
6子智能体翻找文件的过程把主对话塞满消耗的 token 成倍上升
7钩子关键检查靠人记得提醒并不可靠调试变难,交互变慢
8压缩对话记录会被长任务撑满有损,可能丢掉关键细节
flowchart LR
  H["外壳 Harness"] --> SP["① 系统提示"]
  H --> CF["② 配置文件"]
  H --> TL["③ 工具列表"]
  H --> PM["④ 计划模式"]
  H --> SK["⑤ 技能"]
  H --> SA["⑥ 子智能体"]
  H --> HK["⑦ 钩子"]
  H --> CP["⑧ 压缩"]

这八层的排法有来由。它们全部由三条原则推出来:简洁,宁可要数量少但能力强的工具;盯住上下文窗口,避开模型开始变笨的那一段;让提示容易改动,因为提示的内容和长度会随模型换代而演变。后面每一节展开一层时,都可以回头对照这三条。

二、发动机与整车:为什么外壳比换模型更重要

这是这一章最该带走的判断,值得展开讲。先看四条理由。

  1. 模型是同质的。所有人调用的是同一批模型接口,能拿到的能力差不多,在模型上拉不开差距。
  2. 外壳是差异化的。同一个模型,配上不同的工具切分方式、不同的上下文策略,实际表现可能相差数倍。
  3. 模型会更新,外壳要跟着适配。模型随时可以换,可工具的语义设计得糟糕,换更强的模型也救不回来。
  4. 钱。上下文越长,推理越贵、越慢,好的外壳直接省钱。
关键结论 八层外壳里,能被你改动的部分远多于你想象:系统提示、工具描述、配置文件、计划模式的提示词、压缩时保留什么,全是文本。 换模型是换一个别人造好的零件,调外壳是调一套你自己写的零件。
生活类比 模型是发动机,外壳是整辆车。你要开三百公里去外地,发动机决定这辆车有多少马力,方向盘、变速箱、刹车、导航、雨刷决定你能不能真的到那里。 一辆装了普通发动机、但转向和刹车都好用的车,能把你送到目的地;一辆装了顶级发动机、却没有方向盘的车,只能原地轰鸣。 两百行的玩具是能发动、能前进的卡丁车,能到,但一路上你得自己看路、自己判断什么时候刹。

这个类比能一路对应到真实零件上。产品外壳的零件全是看得见、改得动的文本:系统提示是一份文档,工具清单是一张表,计划模式的提示词是几段要求,子智能体的任务书是一份模板,压缩用的提示词规定了十来个固定小节。这些零件都在你手上;发动机不在。

顺带说清一件事:从这一章起,默认的工作方式是「一个人,加上几个并行的智能体」。并行是常态,多个智能体同时在不同方向上探索或实现;人的角色变成协调者,分配任务、审查产出、合并结果。上一章讲的沟通能力,到这里对象一半是人、一半是智能体。

关键结论 换模型是换发动机,调外壳是调方向盘。发动机你再换也只有那么几台可选,方向盘你可以一直调到顺手。 遇到产出不好的时候,先检查外壳,再考虑换模型。

三、工具列表:每个工具都是一段要塞进提示的说明书

两百行那个玩具只带三个工具:读文件、列目录、改文件。三个工具刚好覆盖一件事:知道有什么、看到具体内容、改变内容。真实产品以其中一个为例,内置工具约 40 个,按用途分成几组。

分组代表工具作用
文件Read、Edit、Write读、改、新建代码文件与笔记文件
命令行Bash、Monitor执行命令,长时间运行的任务可以边跑边看输出
搜索与代码理解WebSearch、WebFetch、LSP上网查资料,按语义查找符号的定义和引用
智能体Agent、ListAgents、SendMessage派生子智能体,多个智能体之间互相传话
规划与隔离EnterPlanMode、EnterWorktree进入计划模式,在一个隔离的副本目录里试错
调度CronCreate、ScheduleWakeup定时任务与自我唤醒
交付物Artifact 系列发布网页产物,读写它附带的数据
交互AskUserQuestion、Skill向用户提问,加载一个技能

表里有一个词要先拆开:LSP(Language Server Protocol,语言服务器协议),一套统一约定,让编辑器和「懂这门语言的程序」对话。它能直接回答「这个函数在哪定义」「哪些地方调用了它」,不用模型自己在文本里逐字搜索。省下来的搜索过程,等于省下来的上下文。

还有一个:MCP(Model Context Protocol,模型上下文协议),一套开放约定,用来把外部工具和数据源接到智能体上,可以想成「接口的通用插座」。第 5 章专门讲它。接进来的工具会按固定格式起名,形如 mcp__服务器名__工具名。

工具描述本身就是提示

这是本节最容易被低估的一句话。工具的名称、描述、参数格式全都要放进提示里,每一轮推理都要重新读一遍,所以它属于提示,而且是用户永远看不见的那部分提示。

一个坏描述长这样:名字叫 do_thing,描述写「处理数据」,参数写 x 是字符串、y 是任意类型。模型看完完全不知道处理什么数据,x 是路径还是编号,y 能传什么。它只能猜,猜错的概率不比不写低。

好描述要交代四件事:什么时候用、什么时候不用、参数的格式与边界、返回什么。实践建议是把工具描述写到「一个刚入职的工程师看完就知道该不该用」的程度。这一步的回报比反复打磨聊天里的措辞高得多,因为聊天措辞只影响一次,工具描述影响每一次。

工具越多,注意力越分散

每多一个工具,能力上限提高一点,注意力被稀释一点。原因很直接:工具的名字、描述、参数格式全部进入提示,每一轮推理都要重新读。假设一个工具占 100 个 token,40 个工具就是 4000 个 token,这些全是说明书,正经的活还没开始干就花掉了。

更麻烦的是选择变难。多个工具功能重叠时,模型会犹豫甚至选错。有一个很直接的判断标准:如果一个人类工程师都不能确定该用哪个工具,就别指望 AI 能确定。这个现象有个名字叫工具膨胀。

常见坑:以为装得多就是强 新手喜欢一次接十几个外部工具,觉得能力越大越好。实际结果是每轮推理都要先读完十几页说明书,速度变慢、成本变高、选错工具的概率上升。 判断办法很简单:这一周里真正用到的有几个?用不到的先断开,需要时再接回来。

四、系统提示:行为、护栏、环境

系统提示是每一轮推理都会重新加载的最高优先级指令,可以想成这个智能体的岗位说明书。它定义三样东西。

第一样是行为。输出格式、工具选择偏好、并行策略。真实产品的系统提示里会写:你输出在工具调用之外的文字会以普通文本格式显示给用户;能用专用工具的时候优先用专用工具,少用命令行的临时办法;彼此独立的工具调用可以在一次回复里并行发出。这些细节决定产出读起来顺不顺、跑起来快不快。

第二样是护栏。guardrails(护栏)这个词来自公路边的护栏:它不阻止你开车,只在你快要冲出路面时把你挡回来。放进智能体里,护栏是一类硬性约束,包括拒绝协助某些类型的请求、执行不可逆操作前必须先获得授权、删除或覆盖文件之前先看一眼目标是什么。护栏写在系统提示里,因为那是优先级最高的位置;靠用户每一轮临时提醒,迟早会忘。

第三样是环境。这一项最常被低估。新手以为系统提示主要在写「你是一位专业的助手」这类人设,实际真正有用的是环境描述:我在什么操作系统上、在哪个目录、这个目录是什么项目、有没有网络、现在几点。缺了这些,模型会做出很荒唐的假设,例如在一个只认另一套命令的系统上写出完全不能执行的指令。

系统提示里通常还有一小节专门说明「对话变长时会发生什么」,提前告诉模型历史会被摘要替换。这句话看起来多余,实际是替压缩那一层铺路:等到历史突然变短,模型不会以为自己失忆了。

生活类比 你去食堂兼职第一天,后厨墙上贴着三张纸。第一张写岗位:你负责打饭窗口,两荤一素,米饭压平。 第二张写规矩:生熟分开,戴帽子,掉地上的不许捡起来再用。 第三张写环境:今天是新生报到日,人多,米饭提前多蒸两锅。 三张纸缺哪一张都会出问题。缺第一张你不知道站哪,缺第二张会出事,缺第三张中午就要被排队的人骂。

五、配置文件与计划模式:把约定和方案先写下来

系统提示由工具厂商写,覆盖所有用户和所有项目;配置文件由你或者你们团队写,只管一个项目或者一个人。配置文件的指令优先级高于模型的默认习惯,可以在里面覆盖掉它的默认行为。

最常见的两个名字是 AGENTS.md 和 CLAUDE.md,放在项目根目录,每次会话自动加载。结构值得照抄,只有四块:这个项目是什么、目录怎么排、常用命令有哪些、代码风格是什么。四块分别解决四个麻烦:模型不知道全局、不知道该去哪找文件、不会用这个项目、产出不符合规范。还有一句提醒同样重要:只写项目上下文必需的最小指令。把整本规范手册塞进去,等于让它一直占着有限的地方。

计划模式解决另一个麻烦。没有计划模式时,你说「给项目加上用户登录」,它立刻开始改代码。改到一半你才发现它选了你不喜欢的登录方式、动了你不想动的数据表、引入了你不想要的第三方库。

计划模式的本质是一次便宜的确认:让智能体先用文字写出打算怎么做,你审一页文字,比审几百行改动便宜得多。真实产品的计划模式提示词里有五个阶段:先读代码并向你提问,基于探索结果设计实现方案,读关键文件澄清疑问,写出最终计划,提交计划等你批准。第一阶段强制只能做只读探索,任何编辑都被禁止,这条约束的优先级高于其他一切指令。

计划模式提示词里还有一条容易被忽略的要求:动手之前先搜索项目里已有的函数和工具,避免重复造轮子。AI 生成的项目经常出现重复实现,一个常见原因就是没有人要求它在计划阶段先找现成的。

六、技能、子智能体、钩子、压缩

剩下四层各自对付一个具体的麻烦,代价也各不相同。

技能是一段由智能体执行的书面流程,它可以带工具调用,也可以带本地脚本调用。它和普通提示的差别在生命周期:提示是一次性的,技能被固定下来、反复使用。技能的设计关键是按需加载:假设有 50 个技能,每份文档 3000 字,全部预先加载就是 15 万字,上下文直接爆掉。实际做法分三层:所有技能常驻上下文的部分只有名字加一句话描述,几十个技能也只占几百个 token;判断某个技能相关时才加载它的正文;正文里指向的参考文件和模板,需要时才逐个打开。这跟人查手册的顺序一致:先看目录,再翻到那一章,最后才看附录里的表格。

子智能体解决的是上下文被弄脏的问题。假设你要改一个陌生大项目里的某个功能,需要读 40 个文件才能理解现状。主智能体亲自读,这 40 个文件的内容会全部留在主对话里,真正干活时它已经在变笨的区域里工作。换成子智能体,读这 40 个文件发生在一个全新的上下文里,读完总结成一份一千多字的结论交回来。观察到的数量关系是:子智能体可能消耗几万 token 去探索,回传给主智能体的通常只有一两千 token 的浓缩摘要。花 token 换主上下文的干净程度,因为主上下文的质量直接决定后面每一步判断的质量。

flowchart LR
  T["主上下文:任务与约束"] --> S["派生子智能体"]
  S --> R["独立上下文:读 40 个文件"]
  R --> C["浓缩结论约 1 到 2 千 token"]
  C --> T

子智能体通过一次显式的工具调用启动,所以主智能体要先判断「这件事值不值得派一个分身」。判断依据通常是三条:这个子任务要读大量信息但结论很短,值得;子任务和主任务相互独立、可以并行,值得;子任务只有三行结论、读两个文件就够,自己干更快。代价也要说清:token 总量成倍上升,还要额外付出协调开销。把子智能体当成「开更多线程就更快」是常见误区,任务之间不独立、或者需要共享大量上下文时,派分身反而更慢更贵。

钩子是在外壳生命周期的不同节点自动插入的提示或脚本。节点指固定的时机:会话开始、你提交消息之后、工具调用之前、工具调用成功之后、工具调用失败之后、子智能体启动或结束时、智能体结束回答时、压缩前后。它解决三类问题。第一类是安全拦截:在工具调用之前拦下危险命令,判断由代码做出,不依赖模型记不记得要小心。第二类是自动后处理:每次改完文件自动跑格式化和检查,失败就把错误信息丢回给模型让它自己修。第三类是上下文注入:在你提交消息时自动附加当前时间、当前分支、最近几次提交,这些是模型需要但用户不会每次都说的信息。代价是:钩子太多,每次工具调用都要多等一会儿;钩子逻辑太重,变成一堆看不见的隐式行为,出问题时很难查。

压缩要对付两个硬约束。第一个是窗口有限,跑几个小时的会话,光是命令回显和测试日志就能累积到几十万 token。第二个更要紧:即使没满,历史太长模型也会变笨。压缩的做法是把消息历史交给模型,让它提炼出最关键的信息,用摘要替换原来的历史,然后带着摘要和最近访问的几个文件继续工作。压缩是有损的,丢弃的内容不一定找得回来,所以安全关键的任务要改用自己定制的压缩要求,明确列出绝对不能丢的信息种类。

生活类比 一学期记了八十页笔记,考前不可能从头读一遍,于是你整理成三页提纲。整理的过程就是压缩,提纲就是摘要。 问题在于,如果你把上课时反复强调过的「这道题不许用计算器」也压缩没了,考试时就会踩坑。稳妥的做法是把不能丢的那几条原话抄在提纲最前面。

七、三条原则与两个观察实验

八层讲完,回到三条原则,每条都能落到一个动作上。

简洁:宁可要一个能力强的通用工具,也不要十个只能覆盖预设场景的专用工具。命令行和数据库查询语句就是例子,一个通用工具能顶十个专用工具。

盯住上下文窗口:把窗口当成有预算的资源来管,警惕五类膨胀来源——装了太多外部工具、探索留下的原始文件内容、几十次命令回显堆在历史里、提示里把同一条规则重复三遍、配置文件写成了手册。

让提示容易改动:把提示分节、把路径和命令外置到配置文件、把提示当代码来管理,能进版本库、能评审、能回退。

下面两个观察实验都不需要安装任何东西,只要能打开一个带智能体的编程工具就能做。

现在就做 十分钟,给手上任何一个项目写一份配置文件,四块就够。
① 在项目文件夹里新建一个文本文件,命名为 AGENTS.md;
② 第一块写这个项目是什么,两句话,说清做什么、给谁用;
③ 第二块写目录结构,只列重要的三到五个文件夹,每个后面跟一句说明;
④ 第三块写常用命令,例如怎么运行、怎么跑测试;
⑤ 第四块写代码约定,例如「变量名用完整的英文单词」「不要为了一个小功能引入新库」。
写完让智能体在这个项目里做一件小事,看它还会不会犯以前常犯的那个错。用记事本就能编辑,写坏了删除文件即可,不影响项目里的其他内容。

自测

1. 一个智能体由哪几部分组成?八层按顺序说一遍。
一个模型、一个循环、八层外壳。八层依次是系统提示、配置文件、工具列表、计划模式、技能、子智能体、钩子、压缩。循环的骨架从头到尾没有变,全部增量都在外壳这一层。
2. 为什么说调外壳比换一个更强的模型更划算?至少说出两条理由。
模型是同质的,所有人用的是同一批接口,拉不开差距;外壳是差异化的,同样的模型配上不同工具切分和上下文策略,表现可能相差数倍。另外模型随时能换,但工具的语义设计得糟糕,换更强的模型也救不回来;上下文越长成本越高,好的外壳直接省钱。
3. 工具描述为什么也算提示?一个坏的工具描述会造成什么后果?
工具的名字、描述、参数格式全都要放进提示里,每一轮推理都重新读一遍,所以它属于提示,而且是用户看不见的那部分。坏描述让模型不知道该在什么时候用这个工具、参数该传什么,结果是选错工具或者传错参数,同时白占上下文空间。
4. 系统提示定义哪三样东西?其中哪一样最容易被低估?
行为、护栏、环境。最容易被低估的是环境:我在什么系统上、在哪个目录、这个目录是什么项目、有没有网络。缺了环境描述,模型会做出荒唐的假设,例如写出在这台机器上根本不能执行的命令。
5. 子智能体和压缩分别在对付什么问题?各自的代价是什么?
子智能体对付探索过程污染主上下文:让翻找文件这类脏活发生在独立上下文里,只把浓缩结论带回来,代价是 token 总量成倍上升,还要额外的协调开销。压缩对付窗口被撑满以及历史变长后模型变笨,做法是用摘要替换历史,代价是有损,可能与安全相关的约束会被摘要掉,所以要把不能丢的内容逐字保留。