造一个对话 agent — 上下文、循环与 UX
这一章讲三件事: 对话型任务要带哪四种料、各留多少;怎么用十几行代码把 前两章的零件拼成一个能一直聊下去的 agent;以及用户盯着的那块屏幕, 为什么直接决定他敢不敢让 agent 动手。 这是原书第 8 章的下半场:能力(第 10 章)已备齐,这一章负责组装。
1. 这一章讲什么
第 10 章结束时,模型有了手(工具)和草稿纸(思维链), 但每次互动还是「问一句、答一句」。想让它连着办一串事, 还缺一样东西:这一次对话攒下的上下文1。
「上下文」听起来像聊天记录的代名词。这一章会把它拆成四种料—— 只有分清哪句是背景、哪句是现场指令,才知道该多远留多少。
主走查接着用恒温器:同一间屋,连续三轮人话 (「热死了」「猛降温」「给我调回去」), 看 agent 靠什么接住每一轮——答案分别是常识、字面理解和记性, 而「记性」正是本章的头号机制。
2. 顶层全景:一个循环,一份不断变长的文档
用户输入 ──→ messages 列表(整段对话文档)
│
▼
process_messages(第 10 章的五步)
│ 模型回话:要调工具吗?
│ ├─ 要 → 应用执行 → 结果也追加进 messages → 再问模型一轮
│ └─ 不要 → 把模型的答话追加进 messages,打印给用户
│
▼
回到「等下一个用户输入」
图说:所谓 agent,就是外层「等用户说话」和内层「跑 process_messages」
两个 while(满足条件就一直重复的循环结构)。模型每一步看到的,
只是这份越来越长的 messages 文档。
作者的原话是:第 10 章演示完工具调用时,离完整的对话 agent
只差两件事——给用户一个输入口,再把 process_messages 套进一个循环里2。
没有第三件。
3. 核心原理
3.1 主走查:三轮人话,三 种本事
主走查开始。 还是那两个工具(get_room_temp / set_room_temp),
但这次用户不配合机器语法了3:
第 1 轮 用户:「Golly gee, it's hot in here. Let's cool this place down.」
agent:[调用 get_room_temp() → 返回 64ºF]
agent:「The current room temperature is 64ºF, which is actually
quite cool……您确定还要降温吗?」
第 2 轮 用户:「Make it LOTS cooler.」
agent:[调用 set_room_temp(temp=50)]
agent:「已经设到 50ºF,很 快就会凉快下来!」
第 3 轮 用户:「Whoa……有点过了吧?Put it back to where it started.」
agent:[调用 set_room_temp(temp=64)]
agent:「已经调回 64ºF。」
图说:三轮对话对应三种本事:第 1 轮靠常识听出「64 度其实不热,
这话有蹊跷」;第 2 轮靠字面服从执行「LOTS」;
第 3 轮靠先前的对话,把「started」还原成数字 64。
第 3 轮是全书的点睛之笔:调回 64 这个数,任何一条当前消息里都没出现。
它只能来自先前对话(prior conversation)——从这个对话一开始,它读过一次 get_room_temp
返回的 644。把那段历史扔掉,「调回去」就无从谈起。
顺带值得停一秒的是第 1 轮那句「其实挺凉的」。 模型对人类体感、季节、建筑环境的全部推断, 没有任何一条写在提示词里——作者管这叫常识免费赠送4。 训练集替你交了这笔学费;你要做的只是别让它失聪(别喂错料)。
3.2 对话这份文档由四种料组成
第 06、07 两章教的是「给补全模型挑料」;到了 agent 这儿,料还是那些料, 容器换成了对话记录——全部四种料都装进这份壳里1。按离「当下」从远到近:
| 料 | 装什么 | 类比 |
|---|---|---|
| 开场白(preamble) | 设定行为的规则、期望与工具定义,一般进系统消息;必要时附 few-shot 示范5 | 上岗培训 |
| 先前对话(prior conversation) | 从头到上一轮为止的全部往来消息及其附件5 | 会议纪要 |
| 挂件(artifact) | 挂在某条消息上、与谈话相关的一块数据——航班列表就是典型5 | 夹在纪要里的附件 |
| 当 前往返(current exchange) | 用户此刻的请求 + 它引发的全部工具调用与结果;直到 agent 给出面向用户的答话才算结束5 | 正在讨论的这一页 |
书里的表 8-1 是个旅行助手,四个位置各走一遍6:
[开场白/系统消息] "You are a helpful and knowledgeable travel assistant.
The current date is 8/9/2023."
[先前对话] 用户:「Are there any flights from Dulles to Seattle next Monday?」
助手:两班 + <artifact>(JL5441 周一 9:20AM / AS325 周一 4:50PM)
[当前往返] 用户:「Are there any tickets available first one?」
助手:[调用 get_ticket_info(flight_num="JL5441")]
工 具:{"price": 350.00, "currency": "USD",
"stops": ["ORD"], "duration": "7h40m"}
[最终答话] 「有一班 $350,经停芝加哥。」
注意第二处用户输入只说了「first one」。 「first one」指哪一班?线索躺在先前对话的那个 artifact 里。 这个例子同时演示了挂件的两个用法:上一轮把航班列表挂出来, 下一轮当前往返就能指着它说「第一个」6。
3.3 挑料与摆料:每一样都在跟预算谈判
怎么从这四种料里取舍,作者先亮态度再列清单: 没有万能配方,唯一可靠的纪律是反复评估(evaluate, evaluate, evaluate)7。 清单本身是一条条权衡8:
- 工具要不要在场? 某些阶段明知用不上某件工具,就把它撤下—— 给模型减一件分心的东西8;
- 挂件全给还是让模型挑? 全给,信息最全但杂音大; 让模型挑,要多搭一次「请你选哪些相关」的旁路请求,应用复杂度上一个台阶9;
- 挂件怎么呈现? 直接塞正文(XML 标签或 markdown 小节)即可, 作者的口碑是格式影响不大——但要自己验。还有个白捡的技巧: 如果挂件本来全都来自函数调用,原样保留那些调用,等于顺手给模型 多看几遍工具示范10;
- 单件挂件切多大? 整本书当然塞不下。接力第 06 章的弹性片段:
作者提了一个自己都注明「还没试过」的巧招——摘要每条后面缀一句
「详情请调
details('section 5')」,模型追要哪节,应用就把哪节展开11; 更传统的做法是对大挂件挂一套检索(RAG)11; - 先前对话留多远? 话题换了就该丢。怎么知道换没换? 要么按时间一刀切(隔了一阵的旧会话一律丢弃), 要么训一个小模型专门判相关性——大模型干这事太贵太慢12。
作者收尾时的坦白值得原文照录意思:要是当初能把话说死就好了—— 可惜不能。多放怕撑爆提示词又贵又慢,少放怕它瞎猜; 唯一的盼头是模型越来越强、窗口越来越大, 将来也许真能「拿不准就全塞给它」。在那之前,还是那三个词:评估、评估、评估13。
3.4 组装:十几行代码的事
如顶层全景所示,run_conversation 就是围着 process_messages 转的两个圈14:
外圈读用户输入、空字符串即退出;内圈反复跑五步流程,
最后靠「模型既给了答话、又没要调工具」这个条件退出、打印给用户14。
第 10 章那张手绘时序图放到这里依然成立,只是循环的入口从「脚本跑三次」
变成了「永远等下一个输入」15。
作者在这里给出了完整代码(例 8-2)后写了句 "What will you make first?"—— 组装阶段的神秘感到此清零。难的从来不是循环,是上一节的挑料与下一节的界面。
3.5 界面不是装饰品,是授权链路
书里的 UI 一节反直觉地把几件「美观问题」逐一钉成了「信任问题」16:
| 零件 | 解决什么问题 |
|---|---|
| spinner(等待时转圈的动画) | 让用户知道 agent 在干活、没死机16 |
| 工具 pill(消息里的药丸形小标签) | 让用户看见「它在后台调工具」,而不是莫名卡住16 |
| 工具调用可展开(书里用 Dave/HAL 的截图演示) | 答得出乎意料时,点开能看到工具名、填的参数表单、拿到的结果17 |
| 参数表单可改 + 改完重提交 | 用户不只是旁观:改一个参数,对话从那一点起重新生成——纠偏的最小操作18 |
| 危险操作授权弹窗 | 只要调用可能动真实世界资产,执行前必须过一道人工确认19 |
作者举的场景(Dave 与 HAL)值得细看一次: HAL 答非所问,Dave 点开「Tool calls」,发现它拿到的结果有问题, 于是在 webform 里直接改参数重新提交, 后面的对话从头再生一遍——没有一句「抱歉请重新描述需求」18。
最后还有一条留给「挂件」的:让用户看得见 agent 正在看什么。 界面上显示当前挂着的 artifact,谈得拢就继续,发现它盯错了东西 就把那件挂件关掉——注意力对齐,比事后解释便宜得多20。
4. 作者的判断与证据
有硬证据的:
- 三轮恒温器对话是书里印出的真实运行记录,包括 agent 自发的 「64 度其实挺凉」和最后的 set_room_temp(temp=64)3;
- 「离完整 agent 只差一个循环」这句结论附带完整代码(例 8-2), 书里明确说所有代码原样可跑14;
- 四种料的划分与旅行助手例(Table 8-1)是书自带的演示数据6。
必须标明的:
- 挑料的整份清单,作者自认是权衡而非定论, 「evaluate, evaluate, evaluate」出现了两次当作免责声明713;
details('section 5')这个展开挂件的巧招,作者原话注明 「though one that we haven't yet tried」——是设想,不是实测11;- 挂件用什么格式(XML/markdown/纯文本)书里只给口碑 「it doesn't seem to matter much」,并叮嘱读者自己测10。
判断(我们的,不是书里的): 「agent = while 循环」这件事越早破除迷信越好, 因为它把工程难点放回了正确的地方:难的不是自主性,是喂料与止损。 而止损(改参数重提交、关挂件、授权弹窗)全是 UI 层的动作—— 在这本书的框架里,UI 不是产品的皮肤,是控制系统的一部分。 如果错,会错在: 若未来的推理类模型把「规划若干步」 内化成单次输出, 循环的意义会缩水成「执行引擎」;但只要动作仍发生在真实世界, 授权与止损那一层就不会消失。
5. 边界与局限
- 书的 UI 蓝图是文字版:spinner/pill/webform 都是示意, 语音、富媒体场景要自行迁移;书里也没碰日志(系统运行时留下的记录)、鉴权、API 封装这些上线必答题21;
- 对话 agent 有天花板:作者明说它需要人不时拽一把才能不跑偏—— 这句话是通往原书第 9 章(LLM 工作流,我们的第 12 章)的门22;
- 「将来也许可以无脑全塞」被作者自己标注为期许,不是现状; 当下的决定仍然要拿评估支撑13。
6. 可带走的
主走查一行回顾:「热死了」→ 查温 64 并直言其凉 → 「LOTS cooler」设到 50 → 「调回去」凭 prior conversation 还原成 64—— 第一第二轮免费,第三轮花的是此前存对了账。
- agent = 双层循环,不用神化;装它的代码比想象短得多;
- 对话有四种料:开场白、先前对话、挂件、当前往返; 能力来自「料放在正确的坑里」,比如「调回去」吃的就是先前对话;
- 撤下闲着的工具,少一件是一件;危险操作拦在应用层等真人签字(第 10 章规矩在本章落地);
- 大挂件别硬塞:要么可展开(details 型小钩子),要么上检索;
- 旧会话按时限或相关性裁剪,相关性判断交给小模型而非主力模型;
- 把工具调用做成可视对象:pill 显示进行中,点开见全貌,webform 改参重跑;
- 动真钱的动作必须过授权 UI——这条和第 10 章的应用层拦截互为表里;
- 让用户看见 agent 的注意力(当前挂件),发现跑偏当场掐掉;
- 「常识免费」是真的,但它依赖你选对料——错误料会教会模型错误的常识。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 四种料的定义 | Chapter 8 | text/11-ch08-chapter-8-conversational-agency.txt:733(搜「there is a preamble」) · :738(搜「recent back-and-forth」) · :742(搜「attached artifacts」) · :748(搜「current exchange begins」) |
| 旅行助手四料实例 | Chapter 8 | text/11-ch08-chapter-8-conversational-agency.txt:787(搜「Dulles to Seattle」) · :815(搜「350.00」) · :821(搜「flight for $350」) |
| 挑料清单与告诫 | Chapter 8 | text/11-ch08-chapter-8-conversational-agency.txt:832(搜「evaluate, evaluate」) · :838(搜「one less distraction」) · :857(搜「preserve the function calls」) |
| details() 展开挂件 | Chapter 8 | text/11-ch08-chapter-8-conversational-agency.txt:869(搜「section 5」) · :874(搜「traditional RAG」) |
| 先前对话留多远 | Chapter 8 | text/11-ch08-chapter-8-conversational-agency.txt:878(搜「inactive for」) · :889(搜「When in doubt」) |
| run_conversation 组装 | Chapter 8 | text/11-ch08-chapter-8-conversational-agency.txt:901(搜「tossing a loop around」) · :942(搜「process_messages(client」) |
| 主走查三轮对话 | Chapter 8 | text/11-ch08-chapter-8-conversational-agency.txt:972(搜「Golly gee」) · :984(搜「LOTS cooler」) · :996(搜「reset the room temperature」) |
| 常识免费与最后交换 | Chapter 8 | text/11-ch08-chapter-8-conversational-agency.txt:1004(搜「common sense reasoning for free」) · :1010(搜「back to its starting point」) |
| UI 六零件 | Chapter 8 | text/11-ch08-chapter-8-conversational-agency.txt:1045(搜「spinner that indicates」) · :1051(搜「pill button」) · :1062(搜「modify arguments from the webform」) · :1073(搜「authorize any request」) |
| 挂件可见可关闭 | Chapter 8 | text/11-ch08-chapter-8-conversational-agency.txt:1083(搜「dismiss an artifact」) |
| 通向工作流 | Chapter 8 | text/11-ch08-chapter-8-conversational-agency.txt:1096(搜「corrective influence」) |