跳到主要内容

AI Agent 开发实战 — 本课题摘录

读了哪几节: 2.2(基于大模型从零构建并执行 agent)。 全书八章,其余各章本轮没读。

为什么单读这一节:前面读的所有材料——包括另外两本中文书——要么讲成熟框架怎么用,要么讲成熟产品怎么做。这一节是唯一一份"不用框架、把运行器本身写出来"的。它正好补上 dong-shou-zuo-ai-agent 那本书缺的那一块(那本全程用现成执行器)。

它对本课题回答了什么

一句话定义,以及只有四块的结构图

它的定义:所谓 agent 无非是一个能够调用工具并循环运行以实现目标的大模型。

结构只有四块:

目标

模型 ──→ 工具 ──→ 外部环境
↑ │
└───── 反馈 ────────┘

(依据:书 · AI Agent 开发实战:从基础原理到企业级应用 §2.2 —— 书给的定义是「所谓 Agent 无非是一个能够调用工具/函数并循环运行以实现目标的 LLM」,基本组成结构只有目标、LLM、工具、外部环境四块加一条反馈回边)

对比 hands-on 那本的"比链多两样东西"、shen-ru 那本的三元公式——三份材料的定义几乎一致。 这一份最短,而且它把"外部环境"和"反馈"画进了图里,前两份没有。

决定四:agent 是一个只有五个字段的数据类

它定义的 agent 类:

字段是什么
名字标识
模型用哪个
指令系统提示(可以是一个字符串,也可以是一个返回字符串的函数)
工具列表一组普通的函数
是否允许并行调用 / 调用模式两个开关

"指令可以是一个函数"这个细节值得记:它让系统提示可以随状态变化。 这正好是 cowagent"每轮从磁盘重建"和 hermes"拼一次就冻住"之间的那个开关放在哪儿的问题—— 这一份把它做成了字段类型,两边都能实现。

决定四:循环的条件,以及一个反直觉的计数方式

循环条件:历史比开始时长出的条数还没达到上限,并且当前 agent 不为空。 (依据:书 · AI Agent 开发实战:从基础原理到企业级应用 §2.2 —— 运行器的循环条件是「历史比开始时长了多少条 < max_turns 且 active_agent 非空」,即轮数用历史增长的条数来数而不是单独的计数器;循环内五步——取模型结果、把新消息加进历史、检查有没有工具调用、有就处理(可能切换 agent)、更新上下文变量)

"用历史增长的条数来数轮数"是个真实的选择,而且有坑: 一轮如果调了三个工具,历史就会一次增长四条(一条助手 + 三条结果),等于一轮顶四轮。

这跟 qwen-code 那张"往返 / 回合 / 交互"的表正好对照: 这一份把三个层次混成了一个计数器。 对最小原型无所谓,任务一复杂就会误判。

循环内五步:取模型结果 → 把新消息加进历史 → 检查有没有工具调用 → 有就处理(可能切换 agent) → 更新上下文变量。

没有工具调用就跳出。

决定二:工具定义从函数签名自动生成,不是手写

做法:读函数签名,把参数类型按注解映射成结构化类型;没有默认值的算必填;函数的文档字符串直接当描述。 (依据: book=ai-agent-kai-fa-shi-zhan §2.2 基于OpenAI LLM从零构建并执行Agent 事实=function_to_json 从函数签名自动生成工具定义——参数类型按 type_map 映射注解、default 为空的参数进 required、func.doc 直接当 description)

这一条对我们的最小原型直接可用:工具定义不该是一份跟代码分开维护的清单, 它应该从代码本身生成——否则改了函数忘了改定义,模型就会照着过期的说明传参。

代价:文档字符串直接当描述,意味着工具说明的质量等于注释的质量。 而 shen-ru 那本刚说过"大多数调用失败的根因是不知道工具不能做什么"—— 随手写的注释很少会写边界。两条合起来是一个提醒:自动生成省事,但不会自动写好。

决定四:"交接"这件事最小的实现

工具执行完之后可以返回一个新的 agent,循环把它换成当前 agent 继续跑。

没有编排器,没有路由表,只是换了一个变量。 本课题划了"不管多 agent"这条边界,但这一条值得记下来: 多 agent 的最小形态不需要新机制,只需要让工具的返回值能替换掉"现在是谁在说话"。

而这也解释了循环条件里为什么有"当前 agent 不为空"——把它置空就是收工。

一条关于工具定义成本的说明

书里提到:工具定义在底层被注入进请求里,所以工具的长度也要遵循模型的上下文限制,并作为输入词元计数;遇到上下文限制时,建议减少函数数量或缩短参数长度。 (依据:书 · AI Agent 开发实战:从基础原理到企业级应用 §2.1 —— 书说明函数调用信息在底层被注入,函数长度需遵循模型的上下文限制并作为输入 token 计数,遇到上下文限制时建议减少函数数量或缩短函数参数长度)

这条是 cherry-studio"工具太多就折叠"和 shen-ru"工具定义按需披露"的原始动机, 一本实战书用一句话说清了:工具定义是要花钱的输入。

它没回答什么

  • 错误处理——工具抛异常、模型返回非法参数、循环卡住,这一节都没讲。
  • 历史长了怎么办——除了那句"工具定义也算输入词元",没讲压缩。
  • 并行调用——字段里有开关,但没讲什么时候该开、怎么实现。
  • 停止的其它理由——只有轮数上限和"没有工具调用"两条。

坑与代价

  • 它的运行器是照着一个已有的开源实现重写的(书里下一节就讲那个实现),所以它是"从零抄一遍",不完全是"从零想一遍"。 好处是结构是经过验证的;坏处是它没有解释每个设计为什么这么选。
  • 用历史条数当轮数是个会在多工具场景下失真的计数方式(见上)。
  • 调试信息直接用打印语句混在运行器里。 一本教学书这么写可以理解,但它正好反衬出前面那些实现为什么都要一个独立的事件流: 打印是给人看的,事件流是给人和程序一起看的。

    判断(无锚): 我们的最小原型从第一天就该有一个事件流,哪怕它的唯一消费者就是打印函数。 如果错,会错在: 如果原型只跑一两轮、只在终端看,那事件流是纯粹的抽象负担,直接打印更快看到结果。