最近在学习搭Agent,越看越感觉有种开公司的感觉,而一旦拟人化了,好像很多技术名词也都变得通俗易懂、顺其自然了。所以来分享一些对于 Agent 最基础部分的拟人化理解。
首先,我们先区分三个层次。
第一层是 LLM,也就是大语言模型本身,也就是我们熟知的 Claude,GPT,Gemini,Qwen,DeepSeek,豆包等模型。普通用户通常在网页聊天框里使用它们,而开发者更常通过 API 调用它们。
第二层是聊天应用。我们在网页上看到的 Claude, ChatGPT, Gemini,往往已经不只是一个“裸模型”,而是在模型外面包了一层产品系统:它可以管理对话历史,可以调用搜索,可以读文件,可以使用各种工具,也可以把结果整理成适合人看的回答。
第三层是 Code Agent。Claude Code 和 Codex 就属于这一类。它们不只是回答代码问题,而是能进入代码库,阅读文件,修改文件,运行命令,修 bug,写功能,甚至在一定范围内完成一个工程任务。
这篇文章想讲的,就是从最基础的 LLM API 到 Code Agent,中间到底多了什么。Claude Code 和 Codex 看起来像一个持续工作的程序员,但如果拆开看,它们背后的核心并不神秘:一个会失忆的模型,一本不断维护的上下文案卷,一个负责执行工具的助理,以及一套让它们反复循环起来的工程系统。
一,先把人物摆出来
LLM 像一位天才顾问。她很聪明,但是每次电话挂断之后就完全失忆。
对话历史像一本案卷。因为顾问不会自动记住过去发生了什么,所以每次重新拨电话,助理都要把案卷从头念一遍。你之前问了什么,模型之前说了什么,工具返回了什么,都要重新放进这次请求里。
agent loop 像助理。它负责拨电话,听顾问说话,记录内容,执行工具,再把结果抄回案卷里。
provider 像接线员。它负责和模型 API 通信,把模型那边传来的原始事件翻译成我们程序内部能理解的事件。
UI 则像墙上的直播屏幕。用户看到的不是模型本身,而是助理和接线员加工之后递给屏幕的东西。
比如你输入一句:
比较 session.py 和 config.py 哪个长,把结论写进 notes.txt。
助理不会只把这句话发给模型。它会把当前案卷,也就是完整上下文,加上可用工具清单,一起交给顾问。
这就是所谓的 stateless。API 那边没有一个真正持续存在的“会话大脑”。每次请求,模型看到的就是你这次发过去的全部内容。
工具清单则叫 tool declarations。模型知道自己可以“要求读文件”“要求写文件”“要求跑命令”,不是因为它真的有手,而是因为助理在电话开头告诉了它:我这里有哪些跑腿服务。
二,打字机效果不是一字一句,而是一小包一小包
当顾问开始说话时,她的话不是一个字一个字传过来,也不是等整段生成完再传过来。服务器会把内容攒成一小包一小包的增量。一个包裹可能是几个字,也可能是十几个字。
比如你看到的是:
我来读这两个文件,先看 session.py。
真实传输时,可能更像:
包裹 1:我来读包裹 2:这两个文件包裹 3:先看包裹 4:session.py
这些小包裹就是 delta。边生成边传输的方式,就是 streaming。
但是助理拿到这些包裹之后,并不是只做一件事。它其实有两只手。
右手把包裹递给屏幕,所以你能看到文字一点点出现。
左手把这些碎片重新拼成完整消息,因为案卷里不能存一堆碎纸片。将来重新拨电话时,模型需要看到的是完整的一轮对话记录。
所以,同一份内容会走两条路:一条是显示路,生命周期很短,递给屏幕就结束;另一条是控制路,要被收集,拼接,校验,最后写回上下文。
很多 stream collector 做的,其实就是这只“左手”的工作。
三,不是顾问催促着屏幕,而是屏幕在“伸手要”
屏幕通过 async for 这样的消费方式,向上游要下一个事件。
不过这里还要打一个补丁:这不意味着屏幕不要,模型就立刻停止生成。
真实情况更像“录音电话 + 门口信箱 + 被冻结的接线员”:
顾问那头更像对着录音电话说话。短期内,她并不关心你这边有没有立刻读取。她的输出先通过网络来到你家门口的信箱,也就是操作系统的 TCP 接收缓冲区。
注意,这个信箱里放的不是已经翻译好的事件对象,而是原始字节。你的程序内部没有一个主动堆满“翻译好包裹”的仓库。
接线员,助理,屏幕这一整条室内链路,是按需工作的。屏幕伸手要下一个事件,接线员才去信箱取一包原始数据,翻译成程序内部事件,交给助理,助理再记录和转交。
如果屏幕渲染很慢,比如卡了三秒,顾问可能已经说了不少,内容会先堆在沿途缓冲里。等屏幕重新伸手时,它会发现接下来很多包裹都是“伸手即得”,于是屏幕上可能突然喷出一大段文字,然后再恢复正常打字机节奏。
只有当门口信箱和沿途仓库都满了,网络层的流控才会一路传回源头,让对方暂停发送。这就是 backpressure 在网络层真正发生的地方。
但在 LLM 输出这个数据量级里,它通常不是日常现象。一次很长的回复,线上的总大小也往往只是几百 KB,而沿途缓冲可能是几百 KB 到几 MB。所以大多数时候,模型从头到尾都没有真的被“憋停”。
Pull 设计的价值,不是让模型更慢地说,而是让你的程序内部更干净:没有无限增长的队列,没有难以清理的后台工人,用户取消时也不用追杀一堆残留任务。
相对地,还有一种合法设计叫 push + queue。接线员作为一个独立任务一直醒着,主动从信箱取货,翻译成事件对象,再放进程序内部队列里,屏幕要的时候再取。
这不是绝对错误,只是麻烦很多。你要决定队列多大,满了怎么办,取消时队列里的半截内容要不要清空,后台 task 如何优雅退出。
所以两种设计的核心区别是:东西在哪里等。pull 设计让原始字节等在系统缓冲区里;push + queue 让翻译好的事件等在你的程序队列里。
四,工具调用最反直觉:顾问不是在线等结果
接下来是 agent 最关键的部分。
顾问说:“我需要先看 session.py 和 config.py。”
很多人会自然地想象:模型停在那里,等工具结果传回来,然后继续说。
但真实情况不是这样。
模型发出工具调用请求之后,这一次 API 请求就结束了。换成类比就是:顾问留下了一张跑腿单,然后挂了电话。
跑腿单里写着:读 session.py,读 config.py。
这张跑腿单就是 tool_calls。模型停止的原因是 tool_use。
接下来,助理才开始真正干活。它检查跑腿单,确认工具名存在,参数合法,当前模式允许执行。比如 Plan Mode 下可能只允许读,不允许改。
如果两个工具都是只读的,助理可以并发执行。读 session.py 和读 config.py 互不影响,可以派两个跑腿员同时去。
如果工具会改文件,就要谨慎得多。写文件,改文件,跑某些会改变环境的命令,通常需要排序执行,避免两个动作互相踩踏。
工具执行完之后,助理把结果抄进案卷里,而且必须和原来的工具调用一一编号对齐。模型要的是 tool_result,不是人类随手写的一段总结。
然后助理重新拨电话。
注意,这时接电话的是一个全新的顾问。她不记得刚才发生过什么。她之所以看起来接得上,是因为助理把案卷从头又念了一遍:用户请求,上一轮模型的工具调用,两份工具结果,全部都在里面。
所以“把工具结果给模型”,并不是把结果递给一个仍然在线等待的人,而是把结果写入上下文,再发起下一次请求。
这就是 agent loop 的核心:打电话,拿到跑腿单,挂电话,跑腿,写回案卷,再打电话。
循环一直转,直到模型不再开新的跑腿单。
五,屏幕什么时候停,系统又卡在哪里
屏幕其实没有“我觉得够了”的判断。它只会持续伸手要下一个事件,直到助理递出最后一张收工单。
这个收工单可以叫 TurnDone。它会说明为什么结束。
最体面的结束是 end_turn:顾问说完了,也没有新的工具调用。
但结束不只有这一种。达到最大迭代次数,用户按 Esc,电话线断了,模型连续点名不存在的工具,都应该进入同一个收尾出口。
这个统一出口很重要。屏幕不需要理解每种异常的内部细节。它只需要收到一个 TurnDone,然后知道这轮结束了。
如果用户按 Esc,通常还会有一个很小的键盘监听协程。屏幕这边可能正挂着等下一个包裹,它自己听不见 Esc,所以要有一个值班员专门听键盘。听到之后设置取消信号。助理在事件之间,工具批次之间,迭代边界这些检查点看到信号,就走取消收尾。
这里的优雅之处是:取消不是第二套混乱的出口。取消也被翻译成正常事件,最后仍然是 TurnDone。
至于整个系统的瓶颈,要分尺度看。
微观上,包裹和包裹之间的卡点通常是模型生成速度。SSE 解析,Python 转手,终端画几个字,都比模型吐 token 快得多。
偶发情况下,渲染会成为卡点。比如 UI 每收到一个 delta,就把整个 Markdown 全量重绘一次。大代码块,复杂表格,长回答,都会让一帧渲染变慢。这就是为什么很多终端 UI 要做增量渲染,或者限制刷新率。不是每来一个包裹都重画全世界,而是把很多小变化合并到每秒几次刷新里。
宏观上,真正的大头往往不是 streaming,而是两件事。
第一是重念案卷。每一轮工具调用之后,案卷都会变厚。下一轮请求要把更厚的上下文重新发给模型,首包延迟,也就是 TTFT,会越来越重。prompt cache 的意义,就在于让服务端认出稳定前缀,少重复听一遍。
第二是工具本身。读一个文件可能只要毫秒级,跑一组测试可能要几十秒。这个时候 UI 安静等待,不是因为 stream 卡了,而是因为跑腿的人还没回来。
一句话概括:流式传输本身通常不是瓶颈。微观上卡在模型的嘴,宏观上卡在变厚的案卷和跑腿的腿。
六,好的 agent 设计,补丁都打在边界上
如果只看骨架,agent 好像就是:记录案卷,直播输出,执行工具,再继续循环。
这个理解是对的,但不完整。真正好的 agent 设计,难点往往不在主路径,而在边界条件。
第一,跑腿前要验单。
模型可能写错工具名,参数可能缺字段,路径可能不合法,类型可能不匹配。助理不能拿到单子就跑,而要先校验。校验不过,就把错误包装成结果写回案卷,让模型在下一轮自己修正。
第二,工具要有权限和模式。
读文件,写文件,跑命令,联网搜索,删除东西,风险完全不同。Plan Mode 下可以允许模型观察,但不允许修改。真正执行前,也可以要求用户确认。一个 agent 是否可靠,很大程度上取决于它有没有把工具权限设计清楚。
第三,要有事务式提交。
正常结束时,可以把这一轮完整装订进案卷。异常中断时,不能把半截话随便塞进去。已经完整配对的工具调用和工具结果可以留下,半截消息要丢掉,未执行的工具要补说明,取消原因要写清楚。这样下次重新打开案卷,新顾问才能接得上。
第四,要有迭代上限。
模型可能兜圈子,可能反复调用同一个工具,可能连续点名不存在的工具。助理必须有厂规,比如最多 25 轮,连续几次无效工具调用就停。没有上限的 agent,不是聪明,是危险。
第五,要区分只读并发和写操作串行。
读多个文件可以并发,跑多个搜索可以并发。但写文件,改状态,执行命令,要考虑顺序和冲突。这里不是越并发越高级,而是要让副作用可控。
第六,要让失败也变成普通事件。
工具超时,网络断开,用户取消,参数错误,都不应该让系统炸成一团。好的设计会把它们翻译成 StreamError,ToolResultError,TurnDone 之类的正常事件,让 UI 和下一轮模型都能理解发生了什么。
所以,一个 agent 并不是一个持续运行的大脑。它更像一个严谨的办事系统:顾问负责推理,助理负责流程,案卷负责记忆,工具负责行动,UI 负责呈现。
理解这一点以后,很多工程细节就不再像魔法。所谓 agentic,看起来像一个人在连续思考,其实是一次次失忆后的重新接续。
真正支撑这种连续感的,不是模型内部有一条神秘的意识流,而是那本被不断维护,校验,装订,回灌的案卷。
真正顶级的 agent 系统,难点往往不在这条理想循环本身,而在它如何应对现实世界的压力。
第一类压力,是案卷会越念越厚。上下文越来越长,模型每轮重读就会越来越慢、越来越贵,最后甚至超过窗口上限。所以系统需要 prompt cache,自动压缩,旧结果归档,子 agent 分工,以及长期记忆柜。它们本质上都在解决同一个问题:不要把所有东西都塞进当前这本案卷里。
第二类压力,是世界本身不可靠。电话会断,模型会被 max_tokens 截断,服务会限流,程序也可能崩溃。所以成熟系统要有自动重试,断点续写,会话落盘,错误恢复。不是假装不会出错,而是让出错之后还能体面地接上。
第三类压力,是跑腿真的有风险。读文件还好,写文件、跑命令、删目录,都会改变真实世界。所以 agent 需要权限闸、用户确认、hooks 检查岗、文件快照和回滚机制。越强的工具,越需要清楚的边界。
第四类压力,是一个助理有时不够用。复杂任务里,系统可能会提前执行已经拼好的工具调用,把长时间测试丢到后台,甚至派出多个子助理并行搜索、验证、总结。看起来像一个人在思考,其实背后可能是一支小团队在协作。
所以,agent 的基础循环并不神秘;真正困难的是把这套循环放进一个会变慢、会出错、有副作用、还需要协作的现实环境里。理解了这点,再看各种 agent 框架里的 memory,cache,subagent,hook,permission,checkpoint,就不会觉得它们是零散的功能,而是同一个系统在给四种压力打补丁。
夜雨聆风