这是“手搓 Agent”系列架构实践的开篇。后续文章里,我们会彻底抛开重型框架的层层封装,以一条贯穿始终的订房业务线(“西安 10 月 1 日住两晚,下雨订 suite,否则订 standard”)为锚点,把 Agent 底层的流转逻辑逐层扒干净。为了避免后续概念在工程语境里打架,本文先在架构层面对核心组件做一次严谨的定名与拆解。
相比于裸调 API 的普通聊天,Agent 的核心差异在于对外部状态的改写能力。普通对话场景下,当用户抛出“西安 10 月 1 日该订哪家”时,模型不过是按部就班地产出一份步骤说明,全程没有任何副作用(Side Effect)落地;至于后续是否真的去预订、预订出错如何兜底,一概由人类开发者接管。而切到 Agent 架构,一旦你下达“西安 10 月 1 日住两晚,下雨订 suite”的目标,它就会自行调度天气查询与酒店搜索,甚至在经过 Human-in-the-loop 拦截确认后完成最终的扣款落盘。这种差异的本质,并不在于模型本身更聪明,而在于系统是否为模型装配了**工具调用(Tool Use)**的能力,以及底层是否架起了一台支持其自主推演下一步动作的调度引擎。
为了统一技术语境,下文涉及的核心术语将严格遵循以下工程定义:
| 术语 | 架构定义 |
|---|---|
| 模型 | 核心推理引擎(LLM)。在循环中负责输出状态预测,即决定下一步调用哪个工具,并组装对应的参数(Payload)。 |
| 工具 | 本地代码中可被执行的函数实例。未注册进路由分发清单的,模型一概无感知且无法触达。 |
| 记忆 | 独立于调度循环之外的状态字典,用于持久化目标上下文与已推演出的中间结论。每次交互前会被打平并回灌至 system prompt 中。 |
| 循环 | 核心调度状态机(Loop)。其生命周期为:请求模型 → 捕获 tool_calls 并执行 → 结果回灌 → 再次请求模型,直至输出终答或触发异常终止兜底。 |
| 回灌 | 将工具执行的返回值或异常堆栈,以 role=tool 的角色属性结构化拼装,并透传打平至 messages 数组中的操作。 |
| 终答 | 当模型判定任务闭环、不再吐出 tool_calls 时,所直接响应的纯文本内容(content)。 |
| 终止 | 强行打破调度循环,停止后续路由与执行。通常由安全阈值(如最大轮数限制、上下文膨胀触发的 Token 截断)触发。 |
run | 一次完整的任务生命周期。指代目标指令进入调度循环,直至触发任意出口(终答或终止)的完整流转过程。 |
核心判断准则:普通聊天只能按指令产出步骤;而 Agent 则是依赖自身循环机制完成工具调度与上下文回灌,直至吐出终答或被强行终止。
Table of contents
Open Table of contents
一、四大组件:缺一即退化为“瞎聊”
剥离了框架的黑盒后,这四大组件在底层缺一不可,任何一环的缺失都会让整个状态机流转退化为单调的一问一答:
| 组件缺失 | 系统降级表现 |
|---|---|
| 无模型 | 整个工程失去推理中枢,没有任何组件能够根据当前上下文决定下一个该调度的工具。 |
| 无工具 | 哪怕接入了最顶级的模型,它也只能在纯文本层面输出“建议您前往官网预订”,无法对外部系统产生任何副作用。 |
| 无记忆 | 丧失状态持久化能力,导致每一步的上下文都在重置——比如上一轮辛苦搜到的 hotel_id,在下一轮请求中会直接丢失。 |
| 无循环 | 请求链路退化为单次 HTTP Call,收到响应后立刻结束,既不会触发本地执行引擎,也不存在状态回灌与多步推演。 |
最棘手的问题在于,循环(Loop)本身才是划分 Agent 与普通对话的真正分水岭。普通对话是线性、单次的 Request-Response 模式;而 Agent 的底层是一个具备**出口(Exit Nodes)**的并发调度过程。一旦进入循环,要么因为不再有 tool_calls 而顺利抛出终答,要么因为触碰轮数上限或累计 Token 超限,被系统强行终止。
需要再次强调的是,在这个流转体系里,模型始终只负责一件事——预测“我现在需要调用哪个工具”。至于如何安全执行、如何做数据校验、如何回灌异常堆栈、如何锁死出口,这些全部是由开发者手搓的硬核代码来兜底的。
二、状态机流转:一次“订房”的底层切面
为了把抽象的调度循环落到具体的工程切面上,我们来看目标为“西安 10 月 1 日住两晚,先查当天天气,下雨订 suite,否则订 standard”的请求,其一次完整的 run 在底层究竟是如何流转的:
- 首次循环:模型决定调用查询天气的工具,代码层捕获
tool_calls并执行,将天气结果回灌进messages,同时把“西安 10 月 1 日下雨”这一核心结论落盘到记忆中。 - 二次循环:模型根据回灌的上下文决定调用搜酒店工具,代码层再次执行并回灌。此时严格校验提取出的
hotel_id必须合法,且必须来自本次搜索结果。 - 触达出口:模型判断前置条件已满足,不再输出
tool_calls,转而输出终答并请求用户确认(写操作必须经过确认)。在未获得显式确认前,系统处于挂起状态,拒绝向外发送任何真实的预订请求。 - 状态唤醒:用户回复
user: 确认,上下文重新进入调度循环,代码层执行book_hotel,最终落盘成功并返回最终状态,循环正式结束。
试想一下,如果没有接入工具,它只能“口嗨”建议你订 suite;如果没有外置记忆模块,上一轮搜出的唯一 ID 在下一轮上下文截断后根本对不上;如果没有调度循环,系统在查完天气那一刻就直接结束了,根本走不到预订逻辑这一步。
三、剥离滤镜:实战中容易踩坑的认知误区
在实际动手造轮子之前,我们需要先剔除市面上泛滥的营销话术,回归工程本质:
- 根本不存在所谓的“自我意识”。 系统的底层逻辑依旧是冰冷的概率预测——模型根据当前
messages上下文,预测下一步最应该调用的工具并组装参数。只不过这套预测机制被包裹在一个高度自动化的调度循环里,从外部视角看起来仿佛它“长出了手脚”在自主操作。 - 长链路极其不可靠,本质是概率的连乘。 哪怕单次状态机流转的成功率高达 95%,连续执行二十步之后,整条链路存活的概率也只剩可怜的 36%。一线开发中,真正好用的 Agent 架构往往不是单纯依赖大模型的参数规模,而是靠缩短推理链路、提供健壮的兜底工具,并在高危写操作前强制拉起人工确认拦截来兜住下限。
- 系统最大的风险敞口,全看你透传了什么工具。 “查询天气”和“直接扣款”完全是不同维度的权限级别。永远记住一点:没有硬编码注册进可用路由清单的函数,模型是不可能凭空猜测并调用的。
四、架构自测:你的代码真的是 Agent 吗?
与其被各种概念绕晕,不如用以下三个核心特征,对自己手搓的代码做一次架构审查:
- 输入驱动形式:你喂给系统的,是抽象的业务目标(Goal-driven),还是事无巨细、一步一步硬编码的流水线指令?
- 路由分发决策:下一步到底走哪条分支、调哪个函数,是由模型在运行时动态推演的,还是在代码里写死的
if-else分支? - 系统副作用:这套逻辑是否具备向外改写外部状态(如落盘数据、发起真实扣款、提交 PR)的能力,而不仅仅是产出一堆 Markdown 文本?
如果这三个维度的答案都偏向于前者,那么恭喜你,你已经构建了一个纯正的 Agent 雏形。下一篇我们将直接深入代码层面,把核心调度循环彻彻底底地手写出来:看着模型如何吐出工具请求,本地代码如何执行并透传回灌,以及那三个决定生死的出口,到底是怎么被代码锁死的。
一句话总结
剥离黑盒,Agent 的底层无非是模型、工具、记忆、循环四大组件的拼图。模型仅仅扮演一个负责说“我想调哪个工具”的预测节点;而真正的执行落地、状态回灌与出口控制,全仰仗于你亲手糊出的每一行硬核代码。