Agent框架源码精读:主循环、工具调用与记忆管理
最近把 Pi Agent 框架的源码从头到尾过了一遍。起因很朴素手头有个多轮问答 Agent一到第 7 轮就开始忘记前面聊了什么。我一开始以为是模型上下文不够后来怀疑是消息组装出了问题最后干脆把项目源码拉下来按调用链逐段读。这个框架表面上是给大模型应用提供 Agent 能力的 SDK实际读下来它的核心价值不在“调通一次大模型接口”而在于把复杂任务拆成可编排、可观测、可插拔的工作流。这篇源码阅读笔记我不打算逐行贴代码而是把主干拆开Agent 主循环、上下文构建、工具调用、记忆管理。这四条线走通你就基本掌握了框架的设计取舍和扩展边界。适合正在做智能体应用、纠结 Agent 编排方式、以及想在一周内精读一个开源框架的人参考。1. 为什么值得花时间细读这套 Agent 框架源码1.1 框架定位它不是简单的模型调用 SDK很多人提到 Agent 框架第一反应是“封装好了模型调用我只要传 prompt 就行”。真去读源码会发现Pi Agent 把大量复杂度放在“任务怎么拆、步骤怎么编排、结果怎么回流”上而不是模型 API 本身。框架里负责调用模型的部分可能只有几百行真正撑起框架的是 Planner、Executor、Context、ToolRegistry 之间的协作关系。我读下来的判断是Pi Agent 的代码规模适中模块边界清楚适合作为学习 Agent 工程化的样本来精读。它没有把每个能力都做成重量级组件而是保留了“你可以替换掉某个模块但不影响其他模块”的接口风格。这一点很像 Spring Boot 和若依这类业务框架的模块化思路但 Agent 框架更强调运行时决策而不是静态的 CRUD 脚手架。读源码能带来的最大价值是让你在团队里不再当一个“只会调 API 的同学”。比如产品提了一个“让 Agent 自主决定要不要用工具”的需求如果你只懂调用层面会觉得很悬如果你读懂了 ToolRegistry 的注册、校验、调用、结果回填机制就能把需求拆成几个明确的开关和策略配置。1.2 哪些人真正需要这份阅读笔记我梳理了一下最需要读 Agent 框架源码的人是下面三类。第一类是正在做智能体应用的大模型开发工程师。这类人通常已经跑通过 demo但一进入多轮、多工具、长记忆场景就出问题需要理解框架的边界。第二类是要做技术选型或自研 Agent 平台的工程负责人。团队里用哪个框架、框架做不到哪一步都需要从源码层面判断而不是只看 README 里的功能列表。第三类是做 RAG 或传统后端系统、想引入“规划 执行”能力的开发者。这类人读源码不是为了照搬而是想把 Planner 和执行器拆出来嵌入自己的服务里。如果只是随便跑一下 demo确实不需要读源码。但只要你开始调行为、改策略、加自定义工具源码就是你唯一的真相来源。1.3 读源码的收益边界别指望读完全懂读一套框架源码不等于读完就能造出同样的框架。我的真实体会是读源码最大的收益是建立“边界感”知道哪些能力是框架给的哪些能力要自己写哪些坑是框架设计时就注定的。举个例子Pi Agent 的主循环里如果做了失败重试那重试多少次、重试间隔多久、重试之后是回到原步骤还是重新规划这些策略代码可能只有几十行。但你只读文档永远不知道这个策略藏在哪里。只有读源码你才理解为什么有些任务会“卡住”而不是“报错退出”。所以这份笔记不会罗列每个类的注释而是围绕“一条请求进来之后框架内部到底经历了什么”来展开。2. 整体架构拆解一条请求在框架里怎么走2.1 目录结构与模块边界读一个框架第一步不要急着看某个算法先看包结构。我读的主线版本里目录划分大致如下pi_agent/ ├── core/ # AgentContext、消息、状态模型、事件定义 ├── planner/ # 任务规划器负责生成 Plan ├── executor/ # 执行器负责运行 Plan 里的步骤 ├── tools/ # 工具注册、参数解析、结果标准化 ├── memory/ # 短期记忆、长期记忆、存储适配 ├── llm/ # 模型接入、流式输出、token 计数 └── io/ # 输入输出格式化与校验把每个模块的职责和推荐阅读顺序整理成了表格方便对照模块核心职责建议阅读顺序coreAgentContext、消息结构、状态生命周期1planner生成多步计划、处理规划失败2executor执行步骤、调度工具调用、处理结果3tools工具注册、参数校验、输出标准化4memory短期/长期记忆、存储适配5llm模型接入、流式输出、token 计算6io输入输出校验、错误消息包装7我建议从 core 和 planner 开始读。原因是这两个模块定义了“数据长什么样”和“任务怎么拆”后面 tools、memory、executor 都是围绕它们跑的。如果你一上来就钻到 tools 里研究某个 JSON Schema 生成逻辑很容易迷失在细节里。2.2 核心对象AgentContext、Plan、StepResult整个框架中有三个对象值得先记住AgentContext、Plan、StepResult。AgentContext 不是简单的 dict而是一个有生命周期的状态载体。它贯穿一次完整任务的始终里面通常包含当前问题、已执行步骤、中间变量、工具结果、消息历史。框架里各模块不直接互相引用而是通过读写 AgentContext 来协作。这个设计对我启发很大因为很多自研项目里不同模块喜欢直接调用彼此的方法最后变成一团乱麻。Plan 是 Planner 输出的有序步骤列表。它不是普通数组每个步骤会自带前置条件、依赖关系和成功标准。比如一个“查天气再决定穿什么”的任务Plan 里可能有两个步骤第二步依赖第一步的结果如果没有第一步成功第二步就不该执行。StepResult 是每一步执行后的结果包包含成功标志、输出内容、错误信息、耗时和 token 消耗。这个对象是排查问题的关键。框架里几乎所有观测指标都来自 StepResult如果你看完源码后只能记住一个对象我建议记这个。2.3 数据流从用户输入到结果返回一条请求在框架里的流转可以用一段有序列表讲清楚用户请求进入 Agent.run 入口。AgentContext 被初始化当前用户输入、历史消息、可用工具列表都会写进去。Planner 读取 AgentContext生成 Plan。主循环进入迭代Executor 取出当前需要执行的步骤。如果步骤需要调用工具Executor 从 ToolRegistry 找到对应工具校验参数并执行。工具执行结果写回 AgentContext作为后续步骤的输入。循环检查终止条件计划是否全部完成、是否达到最大步数、是否收到取消信号。最终结果从 Context 中汇总经过 io 模块格式化后返回给调用方。这套流程里最重要的一句话是所有东西都在 Context 里流转模块之间不要直接强依赖。你读源码时会发现新增一种工具不太需要改动 Planner新增一种记忆策略也不用动 Executor原因就在这个总线设计上。3. 核心链路解析Agent 主循环里最值得记住的三件事3.1 主循环不是 while True很多 Agent 实现就是把“调用模型、拿到回复、再调用模型”写成一个死循环。看似简单实际上隐患很大。Pi Agent 的主循环虽然看起来也是重复迭代但有几个关键约束最大步数限制、失败退出条件、取消事件检查。最大步数限制很好理解就是防止 Agent 在一件做不到的事情上反复消耗 token。源码里通常对应一个 max_steps 参数。我实测下来如果任务需要超过 5 步大概率是规划拆得有问题而不是模型能力不够。失败退出条件更值得留意。有些框架一旦某一步失败会默认重试同一个步骤但 Pi Agent 这样的成熟框架会把选择权交给策略是重试原步骤还是重新生成计划还是直接结束并返回部分结果。这个分支逻辑藏在 executor 或策略回调里。如果你遇到 Agent 死循环基本就是这里没有配好。取消事件检查往往被忽略。长任务跑起来之后用户可能已经不想等了。如果主循环不检查取消标志就算外部把任务标记成取消内部还是会把整个 Plan 跑完。我在源码里看到类似 cancelled 的状态检查时才意识到一个可中断的 Agent 比一个“跑得快的 Agent”更重要。3.2 上下文组装动态裁剪比塞满更策略读上下文构建这段代码时我最大的感受是Agent 框架并不粗暴地把所有历史塞给模型。每条消息进入模型之前都要经过组装、裁剪和格式化的过程。典型组成顺序是系统提示词、工具定义、历史消息、当前问题。但工具定义的 token 开销非常大一个完整的 function calling schema 可能占几千 token。如果把所有工具每次都全量发过去上下文很快就爆了。源码里通常会有 token 预算机制。比如给历史消息分配 60% 的预算给工具定义分配 20%给当前请求留 20%。裁剪历史消息时不是简单按字符切而是按“完整消息边界”切避免把一条 JSON 从中间截断。我在笔记里专门记了一条不要为了保留更多历史而不发工具定义。因为模型在不知道有哪些工具的情况下很容易选择纯文本回答根本不调用工具。这一点如果不读源码很难从文档里领会。3.3 Planner 和 Executor 为什么要分离最初我觉得 Planner 和 Executor 分开是过度设计自己写一个编排函数也能完成。后来读到失败重试场景才明白分开是为了“重新规划”这个动作。如果规划和执行耦合在一起某一步失败之后系统不知道是该调整后续步骤还是重新开始。分离之后Executor 可以抛出 StepExecutionError然后由上层决定重新调用 Planner。这种设计让失败恢复变得可控而不是在代码里到处写 if 分支。分离还带来一个好处Plan 可以被记录、回放、评估。你可以把某次任务的 Plan 存下来之后做回归测试看看同一个问题在不同模型版本下的规划是否稳定。这对做 Agent 评测非常重要后面我会单独讲。4. 工具调用与记忆管理框架的两条隐蔽主线4.1 工具注册把“万物”收敛成统一协议Agent 框架里最活跃的部分不是模型调用而是工具调用。Pi Agent 的核心思路是把所有工具都收敛成同一种协议一个名字、一段描述、一个参数 Schema、一个执行函数。我按源码里的调用链整理了一个最小注册示例registry ToolRegistry() registry.register def get_weather(city: str) - dict: 查询指定城市的天气 return { city: city, weather: 晴, temperature: 22, }这个设计的关键在于模型不需要知道工具内部怎么实现它只要看到 JSON Schema 就知道什么时候该调用、需要传什么参数。框架内部会把函数签名转成 function calling schema并在调用后把结果标准化成一个 dict。我在实际使用中最看重的是“统一输入输出协议”。只要每个工具都返回 dict后续的上下文组装和步骤依赖就都能统一处理。一旦有人返回字符串、有人返回对象、有人直接打印日志整个数据流会立刻变得不可控。4.2 参数绑定与结果回填工具链路上最容易翻车的一环模型返回的内容不总是干净的 JSON。参数名可能差一点点类型可能不对甚至会把参数和描述混在一起。框架里的参数绑定机制承担了大量“容错”工作。源码中通常会用类似 Pydantic 的模型做参数校验。如果模型生成的参数无法通过校验框架会返回一个错误给模型让它重新生成。这地方要注意重试次数一旦设置过大就会变成反复调用模型token 消耗成倍增加。合理的做法是重试两次以内再不通过就直接走人工兜底。结果回填也不只是把 dict 放进上下文。框架会记录这个结果来自哪个工具、对应的 request id、耗时等元信息。这样后续步骤才能知道某个字段的数据是否可信、是否需要过时处理。4.3 记忆模块短期、长期、工作记忆的三层划分读记忆模块时我发现很多开发者把“历史消息”直接当成“记忆”这是理解上的偏差。Pi Agent 这类框架通常会把记忆拆成三层短期记忆负责会话内的消息窗口通常是一个有上限的 Buffer比如保留最近 20 轮。实现简单但问题是超出的历史会被丢弃。如果你的业务要求“从第 30 轮还记得第 1 轮的信息”纯 Buffer 是不够的。长期记忆负责跨会话持久化。它需要存储介质可能是内存、Redis、SQLite也可能是向量数据库。源码里通常会把它封装成统一的 MemoryStore 接口里面有 save、load、search 三个核心方法。切换存储后端时上层代码基本不用动。工作记忆则是当前任务过程中产生的临时状态比如已经查到的中间结果、当前正在处理的变量、待验证的假设。这个记忆不需要持久化任务结束就可以清空。我自己的项目踩过一个坑一上来就引入向量库做长期记忆结果发现真正的问题是短期记忆窗口太短。先定义清楚每一层记忆的职责再去选型比迷信某一种组件更重要。5. 源码阅读方法我是怎么做笔记和定位关键代码的5.1 先找入口文件而不是按目录顺序读读源码时最容易犯的错误是按目录顺序从 top 读到 bottom。读了一个小时还在看工具类完全不知道主流程长什么样。我的做法是直接找入口文件。Python 项目通常会有一个main.py 或 cli.py服务型框架会有一个 server.py 或 factory.py。找到 Agent.run 方法之后用 IDE 的“查看调用层次”功能把第一轮调用链路拉出来。第一遍只读链路不读实现。等你知道大致的路径再回头精读每个环节。这个方法对新手特别有效。因为第一遍你可能不知道为什么要有 Planner但你看到 run 方法里 Planner.plan 被调用之后下一步必然走到 Executor.execute这时你就建立起了最基础的调用关系。5.2 让我眼前一亮的三个细节这套框架里有三个细节让我印象很深。第一个是“冷热提示词分离”。框架会把策略性提示词和业务提示词放在不同的模块里。调整某个任务的业务提示词时不需要碰通用策略降低了改动风险。第二个是工具调用超时和重试被做成了显式配置。框架在 Executor 里会检查每个工具调用的耗时超过阈值就触发重试或取消。这样 Agent 不会因为一个外部接口卡住而无限等待。第三个是事件追踪结构。框架里几乎所有关键节点都会发出结构化事件比如 plan_created、step_started、tool_result 等。排查问题时不需要靠日志字符串匹配直接订阅事件流就能还原一次任务的完整轨迹。这个设计我会直接用回自己的业务里。5.3 问题清单法带问题进入源码读源码之前我会先列一个“问题清单”每一条都是一个具体疑问然后在读的过程中逐个回答。这样读起来不容易迷失笔记也有明确的产出。我通常会先用表格记录问题问题涉及模块读完后结论多轮会话历史在哪个阶段被写入memory/context在每轮模型调用结束后写入 Buffer工具返回结果如何被截断tools/executor按固定长度截断保留元信息并行调度由谁负责executor由异步任务池统一调度失败重试会重新规划吗planner/executor由策略决定默认重试当前步骤这个方法让源码阅读从“看书”变成了“破案”效率高很多。6. 从源码到项目落地最小 Demo 与调参逻辑6.1 最小可跑 Demo半小时跑通主流程读完源码之后我建议第一时间写一个最小 Demo 验证理解。这里给出一个符合 Pi Agent 调用风格的示例具体类名以你拉到的版本为准from pi_agent import Agent from pi_agent.tools import ToolRegistry from pi_agent.memory import BufferMemory def get_weather(city: str) - dict: return {city: city, weather: 晴, temperature: 22} registry ToolRegistry() registry.register(get_weather) agent Agent( modelyour-model-endpoint, plannerreact, memoryBufferMemory(max_messages20), max_steps5, ) result agent.run(北京明天适合跑步吗, toolsregistry) print(result.output)这个 Demo 虽然简单但已经把核心链路串起来了Agent 被创建后接收一个任务Planner 生成步骤Executor 调用天气工具结果回填到 Context最终输出答案。我在第一次跑通时特意把 max_steps 设成 3让一个原本需要 4 步的任务提前触发终止条件。这样能直观感受到主循环的边界约束比读十遍 max_steps 的注释都有效。6.2 参数调节的源码依据看了源码之后很多参数不再是无脑设置的而是有明确的依据。BufferMemory(max_messages20) 控制的是短期记忆窗口。如果业务场景需要依赖很早之前的信息这个值要调大但要注意 token 消耗。max_steps5 控制的是主循环最大迭代次数。规划合理时 5 步通常够用但涉及多工具串行加验证的任务可能要到 8 步。工具调用相关的超时和重试参数也值得显式设置。框架默认值往往适合 demo生产环境最好把 timeout 设成 10 秒把重试次数限制在 2 次以内。否则单个工具接口抖动时Agent 会长时间卡住。6.3 把源码知识用回自己的业务读源码最舒服的状态是不需要照搬整套框架只把解决自己问题的部分摘出来。我自己的项目借鉴了三样东西。第一样是 AgentContext 总线状态管理。现在我的服务里不再散落各种临时变量而是统一放在一个带生命周期的上下文对象里极大减少了传参混乱。第二样是 Planner 与 Executor 分离。业务上遇到复杂的异步流程我会先把计划拆成可记录的数据结构再写执行器去跑。这样用户能看到当前走到哪一步运维也能快速定位失败节点。第三样是事件追踪。我在关键业务节点都会发布结构化事件而不是只打日志。排查问题的时间至少省了一半。7. 实战排错我遇到的四个典型问题7.1 工具失败重试导致“死循环”现象是 Agent 反复调用同一个工具每次都报错但仍然反复重试。排查日志后发现每条 step 的请求参数完全一致说明框架在按原参数原样重发而不是重新规划。根因是主循环里没有对失败步骤做“语义区分”。解决方式是在工具执行失败时把错误信息当作一次合法结果回填给模型让模型决定下一步同时设置重试上限。只让模型看到“工具失败”是不够的必须给它两个选择换参数重试或者停止。7.2 历史截断把消息切碎现象是模型回答突然开始输出残缺 JSON或者引用不存在的工具结果。检查 Core 层后发现裁剪历史是按字符长度截断的不是按完整消息边界截断。一条被切了一半的工具调用 JSON 成了上下文里的毒药。解决方式是把记忆裁剪改成“整消息丢弃”优先丢最旧且不依赖当前步骤的消息。同时给工具结果单独设置保留条数避免一个长 JSON 把整轮历史挤掉。7.3 长期记忆拖慢响应现象是每轮对话都要等 2 到 3 秒日志显示记忆模块在每一次模型调用前都做了向量检索。明明上一轮已经查过相同内容数据库层面已经有了但还是会重新检索。排查后确认是长期记忆模块没有做会话内缓存。我在业务上做了一个简单改造把本次会话已检索到的实体和结论放到工作记忆里新的模型调用优先复用工作记忆只有发现信息不够时才触发长期记忆检索。响应耗时直接降了一半。7.4 并行工具结果乱序现象是任务里同时调用了两个查询工具返回顺序不一致导致下一条提示词里的数据互相覆盖。深层原因是框架在异步执行工具时并没有为各个结果附加“步骤标识”回填时按完成顺序覆盖同一个占位字段。解决方式是在 Context 里为每个工具调用分配独立的 slot结果回来之后先存到自身 slot再由执行器决定如何合并展示。这个坑提醒我并行不只是“同时跑”更要把结果边界设计清楚。8. 后续阅读路线与扩展方向8.1 三遍阅读法从跑通到改造读这套框架我不建议只读一遍。我的安排是三遍。第一遍跑主流程不深究细节只看一个请求怎么进来、怎么出去目标是画出一条完整调用链。第二遍精读关键链路重点读上下文组装、工具调用、失败分支把笔记里所有“为什么”补上。第三遍做改造实验比如替换记忆后端或新增一种 Planner 策略。只有动手改过代码才算真正读懂了它。第一遍大概一小时第二遍需要半天第三遍可以结合自己的业务做。三遍下来你对 Agent 工程化的理解会比看任何教程都深。8.2 和传统框架对照时关注点完全不同很多从 Spring Boot、若依这类后端框架过来的开发者第一次读 Agent 框架会不适应。传统框架的核心是数据模型、事务、权限而 Agent 框架的核心是决策循环、状态流转、工具编排。对照阅读时我更关注生命周期管理、可观测性、扩展点、社区维护情况这几个维度。不要去抠某个数据库操作和某个 API 封装因为 Agent 框架里根本没有传统意义的数据库 CRUD它的“存储”大多落在记忆模块里。8.3 后续阅读计划记忆模块与评测落地这篇笔记只覆盖了主干。下一步我会专门拆记忆模块的源码把短期、长期、工作记忆的接口设计和存储选型讲细再结合 Agent 评测落地经验写一份可用指标清单。这套框架里有些设计很适合用 pytest 这类测试工具做回归验证也会一并整理出来。我自己读完一遍的最大体会是源码阅读不是为了背接口而是为了看清框架作者在约束条件下做的取舍。Pi Agent 的代码结构和复杂度都还在适合精读的范围内难的是把规划、执行、记忆、工具这条链路的限制条件想透。下一篇我会把记忆模块的源码拆得更细也会补一组可控的评测实验。如果你也在读这套框架欢迎带着你的问题来聊。