<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Varian Blog</title><description>S, stay foolish</description><link>https://morrowave.me</link><item><title>从 Agent-as-Tool 到 Agent Team：为什么多个 Agent 仍然需要人来协调？</title><link>https://morrowave.me/blog/smolagents-agent-team</link><guid isPermaLink="true">https://morrowave.me/blog/smolagents-agent-team</guid><description>从 smolagents 的委派源码出发，讨论多 Agent 协作中的共同决策、变更影响与交付有效性，以及为什么不必直接照搬人类团队。</description><pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;多个 Agent 能够各自正确地完成任务，却未必能够共同交付一个正确的结果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;研究对象：&lt;/strong&gt; Hugging Face / smolagents v1.26.0&lt;br&gt;
&lt;strong&gt;研究方向：&lt;/strong&gt; Agent-as-Tool · Multi-Agent Collaboration · Decision Consistency&lt;br&gt;
&lt;strong&gt;系列：&lt;/strong&gt; Agent 源码研究 · Part 03&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR：&lt;/strong&gt; smolagents 能把任务交给子 Agent，也能并行处理同一轮的多个调用。但在持续协作中，真正麻烦的往往不是任务怎么分出去，而是任务开始以后，决定又变了。共享聊天记录并不自动形成共同决策；通知成员也不等于调整了已经开始的工作；收齐报告，更不等于完成了当前交付。这篇从委派机制的源码出发，讨论多 Agent 协作究竟需要维持什么，并在最后追问：我们为什么一定要把 Agent 组织成人类式的 Team？&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;写在前面：多了几个 Agent，为什么我还在做交接？&lt;/h2&gt;
&lt;p&gt;我最初用 OpenBot 组织多个 Agent，主要是想减少开发中的等待：与 Codex 讨论方案和实现，再让 Claude 做独立 Review；相互独立的事情，尽量同时推进。&lt;/p&gt;
&lt;p&gt;但工作分出去之后，我依然经常要解释：这项功能为什么这样设计？为什么放弃了另一个方案？Reviewer 这次到底该按什么标准检查？尤其是当我能直接进入某个成员的会话讨论时，一个问题变得很明显：&lt;strong&gt;我在这里改了决定，为什么还要去别的会话再说一遍？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设我们在开发 Web Search。初始要求是：搜索任务超过 30 秒，返回超时失败。Implementer 负责实现，Tester 负责测试，Reviewer 负责审查。&lt;/p&gt;
&lt;p&gt;后来，我和 Implementer 讨论发现：如果多个检索来源中只有一个超时，其他来源已经返回了有价值的结果，直接丢弃所有结果可能不合适。于是确认新原则：&lt;strong&gt;允许返回部分结果，但必须明确标识搜索不完整。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Implementer 开始按新原则修改，Tester 却还在按旧规则写断言，Reviewer 也可能在审旧设计下的提交。各自的工作未必马虎，放到一起却可能互相冲突。&lt;/p&gt;
&lt;p&gt;这个情境只是示意，但它准确表达了我遇到的困惑：&lt;strong&gt;各个 Agent 都在认真做事，不代表它们在做同一件事。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最初我以为这是上下文不够共享。顺着 smolagents 的源码往下看后，我开始怀疑，真正缺少的是对共同决策及其变化的管理。&lt;/p&gt;
&lt;h2&gt;一、从源码看，一次委派解决了什么？&lt;/h2&gt;
&lt;h3&gt;1. Managed Agent：把执行者接到调用接口上&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;agents.py&lt;/code&gt; 的 &lt;code&gt;_setup_managed_agents()&lt;/code&gt; 中，框架按名称注册子 Agent，并为已有实例设置工具调用所需的输入输出元数据。省略描述字段，结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;agent.inputs = {
    &quot;task&quot;: {&quot;type&quot;: &quot;string&quot;},
    &quot;additional_args&quot;: {&quot;type&quot;: &quot;object&quot;, &quot;nullable&quot;: True},
}
agent.output_type = &quot;string&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;子 Agent 没有因此变成普通函数。它仍有自己的模型、工具和执行循环。Manager 需要知道能找谁、交付什么输入、收到什么形式的结果；至于具体怎样完成任务，由子 Agent 自己组织。&lt;/p&gt;
&lt;p&gt;这是一种清楚的分工。但 &lt;code&gt;output_type = &quot;string&quot;&lt;/code&gt; 只规定返回类型，并不保证报告包含它依据的需求、检查的提交或覆盖的验收范围。这些可以由应用层另行约定，却不是接口自动提供的保证。&lt;/p&gt;
&lt;h3&gt;2. 统一调用，不等于共享上下文&lt;/h3&gt;
&lt;p&gt;对 &lt;code&gt;ToolCallingAgent&lt;/code&gt; 而言，普通 Tool 和 Managed Agent 可以进入同一份可调用能力列表。&lt;code&gt;execute_tool_call()&lt;/code&gt; 找到对象、替换状态变量并验证参数后，在字典参数分支中这样分流：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;return tool(**arguments) if is_managed_agent else tool(**arguments, sanitize_inputs_outputs=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Managed Agent 进入自己的 &lt;code&gt;__call__()&lt;/code&gt;；&lt;code&gt;MultiStepAgent.__call__()&lt;/code&gt; 再以任务模板包装输入，调用自己的 &lt;code&gt;run()&lt;/code&gt;，最后将输出包装为报告。打开 &lt;code&gt;provide_run_summary&lt;/code&gt; 还可以附上一部分由运行记录转换的消息，但这不等于两个 Agent 共享一段持续同步的会话。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;run()&lt;/code&gt; 中还有一个容易误读的细节：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if reset:
    self.memory.reset()
    self.monitor.reset()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AgentMemory.reset()&lt;/code&gt; 清除历史步骤，保留系统提示；该分支并不清空 &lt;code&gt;self.state&lt;/code&gt;。而调用方传入的 &lt;code&gt;additional_args&lt;/code&gt; 可以更新 State。因此，复用同一实例时，历史步骤重置与状态变量保留可以同时发生。&lt;/p&gt;
&lt;p&gt;但无论信息保存在 Memory、State，还是外部文档里，&lt;strong&gt;能够读到一份信息，都不等于知道它是否是当前有效的决定。&lt;/strong&gt; 这已经不是单纯的存储问题。&lt;/p&gt;
&lt;h3&gt;3. 并发调用，不等于任务协调&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;process_tool_calls()&lt;/code&gt; 可以把同一轮的多个调用送入线程池，并按完成顺序收取结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;for future in as_completed(futures):
    tool_output = future.result()
    outputs[tool_output.id] = tool_output
    yield tool_output
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用 ID 让框架能够把结果对应回请求。查接口用法和查测试覆盖可以并行；但「Reviewer 必须等实现提交后才能开始」「审查中途验收标准变了怎么办」，不是线程池该回答的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;并发解决已经决定要做的调用怎样一起跑；协调解决哪些工作应当开始、依据什么执行，以及决定变化后怎么办。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;到这里，smolagents 展示的是一套明确的委派机制，而不是一个自动维护项目决策一致性的协议。这个判断针对上述接口与执行路径，不意味着用 Agent-as-Tool 搭建的上层系统不能实现这些能力。&lt;/p&gt;
&lt;h2&gt;二、共享聊天记录，为什么仍然不够？&lt;/h2&gt;
&lt;h3&gt;1. 看见同一段聊天，为什么仍然无法确定要求？&lt;/h3&gt;
&lt;p&gt;回到搜索策略的讨论。聊天里可能先后出现「超时就失败」「是否保留部分结果」「部分结果会不会误导调用方」「确认允许部分结果，但必须标记不完整」。&lt;/p&gt;
&lt;p&gt;把这些话全部交给 Reviewer，确实保留了来龙去脉。但 Reviewer 仍需要判断：哪些只是提议，哪些被否定，哪项要求真正生效。最后出现的一句话，也未必拥有最高的决策权。&lt;/p&gt;
&lt;h3&gt;2. 会话、决策和任务，分别应该记录什么？&lt;/h3&gt;
&lt;p&gt;因此，我更倾向于把三类信息分开：&lt;strong&gt;会话&lt;/strong&gt;保留讨论与理由；&lt;strong&gt;决策记录&lt;/strong&gt;保留已经确认的取舍；&lt;strong&gt;任务记录&lt;/strong&gt;把决定落实为成员可以执行和验收的约束。&lt;/p&gt;
&lt;p&gt;这也是我整理 Repo Harness 时最想解决的事。成员不必共享所有聊天记录，却需要知道与自己任务有关的有效要求；想追究「为什么」时，再回头看讨论。&lt;/p&gt;
&lt;h3&gt;3. 谁能够改变团队的共同决定？&lt;/h3&gt;
&lt;p&gt;不过，把决定写进文档还只是第一步。&lt;strong&gt;谁有权改变它？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Implementer 可以自主调整不改变外部契约的实现细节，也应该能够提出新方案。但如果它打算改变 API 的返回语义，影响了测试、审查乃至调用方，就不该仅凭一个成员的局部判断悄悄替换团队标准。&lt;/p&gt;
&lt;p&gt;不同系统可以赋予 Agent 不同的自主决策权。关键并非凡事都要人工批准，而是要明确：什么属于局部执行选择，什么属于影响共同约束的决定，以及后者怎样确认和生效。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;共享上下文解决的是信息可见性；决策一致性还需要解决信息的效力与权限。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;三、决定生效之后，为什么通知仍然不够？&lt;/h2&gt;
&lt;h3&gt;1. 需要通知的是所有 Agent，还是受影响的任务？&lt;/h3&gt;
&lt;p&gt;假设新的 Web Search 策略已经确认。协调层向相关成员发出通知，是否就完成了协作更新？&lt;/p&gt;
&lt;p&gt;未必。对 Implementer，新要求涉及返回结构；对 Tester，它改变部分断言；对 Reviewer，它改变验收依据。一个处理无关功能的 Agent 则不应被打断。协调层首先需要识别&lt;strong&gt;哪些任务依赖了被改变的决定&lt;/strong&gt;，而不是机械地向所有成员广播。&lt;/p&gt;
&lt;h3&gt;2. 通知已经送达，正在执行的工作怎么办？&lt;/h3&gt;
&lt;p&gt;接下来还有时间差。任务可能尚未开始、正在执行，或已经交付。尚未开始的任务可以更新要求；正在执行的任务可能需要调整或重启，但能否中断还取决于运行环境；已完成的任务则要判断其结果是否仍然适用。&lt;/p&gt;
&lt;h3&gt;3. 局部完成，为什么不等于整体交付？&lt;/h3&gt;
&lt;p&gt;这时最容易犯的错误，是把多个局部完成状态直接相加：Implementer 已提交，Tester 已测试，Reviewer 报告 &lt;code&gt;PASS&lt;/code&gt;，于是整个项目也被标成完成。可如果三份结果依据的是不同的要求或代码，就不能仅靠这些状态证明当前交付有效。&lt;/p&gt;
&lt;h3&gt;4. 旧结果还能用吗？&lt;/h3&gt;
&lt;p&gt;我更愿意让报告至少能回答三个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;决策依据：&lt;/strong&gt; 它按照哪些要求执行？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交付对象：&lt;/strong&gt; 它针对哪份代码或其他产物？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证范围：&lt;/strong&gt; 它实际证明了哪些行为？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如，旧测试证明「所有来源都正常时能返回结果」，如果相关代码和条件未变，这份证据仍可能有价值；但「任何来源超时都必须整体失败」若已被新决定推翻，就不能继续充当当前方案的通过标准。新增加的「部分结果必须标记不完整」，则需要补验。&lt;/p&gt;
&lt;p&gt;这也不是说每次改需求都要推倒重来。恰恰相反，&lt;strong&gt;只有知道旧结果究竟证明了什么，才可能既不误用它，也不浪费它。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果要求和代码还在并发变化，最终写入验收状态时也必须确认依据没有过期。这需要上层协调系统设计版本条件与状态更新规则，不能从 smolagents 的一次工具调用中直接得到。&lt;/p&gt;
&lt;p&gt;所以，真正需要管理的不是消息有没有送达，也不只是哪个 Agent 最后说了 &lt;code&gt;PASS&lt;/code&gt;，而是&lt;strong&gt;有效决策、执行中的任务与交付证据之间的关系&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;四、Team 的价值，是否就在于维持这些关系？&lt;/h2&gt;
&lt;p&gt;现在我更愿意从三个问题理解持续协作。&lt;/p&gt;
&lt;h3&gt;1. 共同决策依据：什么决定有效？&lt;/h3&gt;
&lt;p&gt;成员可以有不同上下文，但必须能取得与自身任务相关的共同约束。&lt;/p&gt;
&lt;p&gt;这并不要求 Coordinator 成为全部知识的主人。项目决策和任务状态可以由持久化的 Repo Harness 等机制维护，协调层围绕这些记录组织执行，Agent 则保留分析和实施的自主性。&lt;/p&gt;
&lt;h3&gt;2. 变更影响管理：哪些工作需要调整？&lt;/h3&gt;
&lt;p&gt;一项决定改变后，协调层需要分析任务依赖、执行进度和影响范围，而不是只把新消息转发给成员。&lt;/p&gt;
&lt;h3&gt;3. 交付有效性验证：哪些结果仍可接受？&lt;/h3&gt;
&lt;p&gt;收齐报告后，还需要判断报告依据的要求、对应的交付物和验证范围，是否仍能支持当前目标。&lt;/p&gt;
&lt;h3&gt;4. 什么时候值得引入这些机制？&lt;/h3&gt;
&lt;p&gt;也没有必要因此把 Agent-as-Tool 和 Agent Team 排成低级与高级两档。以 Agent-as-Tool 为执行方式的系统，同样可以在上层加入状态与决策管理；一个拥有多个长期会话、成员通信和统一面板的 Team 产品，也未必自动解决了一致性问题。&lt;/p&gt;
&lt;p&gt;对于一次独立的代码查询，委派后拿报告就足够。只有当项目持续推进、任务彼此依赖、决定还会在执行中改变，额外的协调机制才可能值得。否则，复杂的记录、同步与复核流程也可能只是把人工交接换成机器流程。&lt;/p&gt;
&lt;p&gt;这些机制给 Team Bot 类产品留下了一个可检验的假设：&lt;strong&gt;明确记录有效决定、追踪其对任务的影响，并核对结果的适用范围，能否在要求变化频繁的协作中，减少人的重复转述和返工？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;要验证它，不能只看 Agent 数量或最终是否完成任务。还应比较人工介入次数、遗漏的受影响任务、误用旧证据的次数，以及协调机制自身的时间与执行成本。现在它仍是设计假设，而不是经过实验确认的结论。&lt;/p&gt;
&lt;h2&gt;五、写在最后：我们为什么一定要模仿人类团队？&lt;/h2&gt;
&lt;p&gt;从一次源码中的委派往外看，我原本想找到一个让多 Agent 更高效的办法。最后却发现，真正拖慢协作的不总是执行速度，而是改变决定之后，谁来让分散的工作重新对齐。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多个 Agent 不必知道同样多的事，却需要在相互依赖的工作上遵守相容且有效的约束。&lt;/strong&gt; 这或许比让每个成员复制同一段聊天历史更重要。&lt;/p&gt;
&lt;p&gt;但写到这里，我又开始怀疑文章一直默认的前提：我们为什么如此自然地把 Agent 组织成 Manager、Implementer、Tester、Reviewer，再让它们像人类团队一样分工、沟通和交接？&lt;/p&gt;
&lt;p&gt;这套组织方式并非毫无道理。它容易理解，方便划定责任，也适合人随时介入。但 Agent 不是人：它们可以按需实例化，读取结构化状态，通过工具交换结果，也不一定需要一段长期成员会话来维持协作。&lt;/p&gt;
&lt;p&gt;或许用户面对的仍然可以是一个「团队」，底层却不必照搬人类开会、转述与交接的方式。协作也可能围绕任务、状态与约束动态形成，而不是始终围绕固定角色运转。&lt;/p&gt;
&lt;p&gt;当然，结构化状态不会自动消除模型对需求的不同理解；动态分工也不会自动解决并发修改、执行副作用或责任归属。抛弃「团队」这个外壳，并不意味着这些难题会消失。&lt;/p&gt;
&lt;p&gt;所以我不想在这里直接宣布一种新范式。至少这篇文章还没有提供足够的比较与实验依据。&lt;/p&gt;
&lt;p&gt;但在努力解决 Agent Team 的协作问题之前，也许值得先问一句：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent Team 究竟是适合 Agent 的组织方式，还是我们借用人类团队经验，为一种尚在形成中的协作范式搭建的过渡形态？&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/huggingface/smolagents&quot;&gt;Hugging Face · smolagents 官方仓库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://huggingface.co/docs/smolagents&quot;&gt;Hugging Face · smolagents 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://huggingface.co/docs/smolagents/examples/multiagents&quot;&gt;Hugging Face · Multi-Agent Systems 示例&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/agent-teams&quot;&gt;Anthropic · Claude Code Agent Teams 文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;注：源码部分针对 smolagents v1.26.0，代码片段经过节选；Web Search 案例是用于说明问题的假设情境，不代表一次真实实验记录。关于决策管理、协调层和结果有效性的内容是分析与待验证的设计假设，不代表上述框架或产品已完整实现这些机制。&lt;/em&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/smolagents-agent-team-cover.BhOu5uwn.png"/><enclosure url="/_astro/smolagents-agent-team-cover.BhOu5uwn.png"/></item><item><title>Agent 的记忆不等于上下文：从 smolagents 看三个关键的架构设计</title><link>https://morrowave.me/blog/smolagents-memory-context-planning-part-02</link><guid isPermaLink="true">https://morrowave.me/blog/smolagents-memory-context-planning-part-02</guid><description>通过 smolagents 的 MemoryStep、summary_mode 与 Soft Planning，理解 Agent 执行历史、上下文视图与规划机制之间的职责划分。</description><pubDate>Tue, 10 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;记录过去并不困难，困难的是决定哪些过去值得参与下一次判断。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;项目：&lt;/strong&gt; Hugging Face / smolagents&lt;br&gt;
&lt;strong&gt;研究方向：&lt;/strong&gt; Agent Memory · Context Engineering · Planning&lt;br&gt;
&lt;strong&gt;系列：&lt;/strong&gt; Agent 源码研究 · Part 02&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;阅读 smolagents 的 Memory 与 Planning 实现时，我发现了一条很值得借鉴的设计思路：&lt;strong&gt;不要把 Agent 的执行历史直接等同于模型的上下文。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它用结构化的 &lt;code&gt;MemoryStep&lt;/code&gt; 保存执行轨迹，通过 &lt;code&gt;to_messages()&lt;/code&gt; 为不同调用构造消息视图，再利用 &lt;code&gt;summary_mode=True&lt;/code&gt; 让 Replanning 排除部分旧推理与历史计划。没有复杂的记忆算法，却把「保存历史」「选择历史」与「利用历史做决策」分开了。&lt;/p&gt;
&lt;h2&gt;写在前面：一个不断增长的 messages 数组&lt;/h2&gt;
&lt;p&gt;在上一篇文章中，我沿着 &lt;code&gt;MultiStepAgent&lt;/code&gt; 的执行循环，研究了 Agent 如何从模型决策走向工具调用，再通过 Observation 获得环境反馈。&lt;/p&gt;
&lt;p&gt;但继续往下读，一个问题逐渐变得突出：Agent 每执行一步，都会产生新的历史。这些历史究竟应该如何管理？&lt;/p&gt;
&lt;p&gt;如果自己实现一个简单的 Agent，最直观的方法就是维护一个不断增长的消息数组：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 伪代码：只展示历史累积的基本思路
messages = [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: task}]

while not finished:
    response = model.generate(messages)
    result = execute(response)

    messages.append(response)
    messages.append(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于简单任务，这样做并没有什么问题。麻烦出现在任务变长之后：Agent 读取了十几个文件、反复搜索、遇到错误，又因为最初的判断不准确而调整路线。此时的历史中，不仅有对当前任务有用的执行结果，也混杂着临时猜测、失败尝试和过期计划。&lt;/p&gt;
&lt;p&gt;全部交给模型，上下文成本不断增加，冗余信息也可能干扰判断；直接截断，又容易丢掉早期的重要线索。&lt;strong&gt;真正困难的不是让 Agent 记住历史，而是决定下一轮模型应该看见什么。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其实在读这部分源码之前，我就一直对 Agent 的记忆机制挺感兴趣。我想做的并不只是一个能够记住聊天记录的助手。相比「上次聊了什么」，我更在意它能否逐渐形成对一个人的理解：知道哪些偏好比较稳定，哪些想法只是随口一提；能够延续之前的讨论，也能在发现新信息时修正旧判断。&lt;/p&gt;
&lt;p&gt;但仔细想想，这件事比简单保存对话麻烦得多。记忆应该在什么时候产生？哪些内容值得长期保留？如果两次对话里的信息互相矛盾，又该相信哪一次？更重要的是，保存下来的东西，真的有必要在每次对话中重新拿出来吗？&lt;/p&gt;
&lt;p&gt;带着这些问题继续读 smolagents，倒是有些意外。它的 &lt;code&gt;AgentMemory&lt;/code&gt; 并没有实现我设想中的长期记忆能力，却从另一个角度给了我启发：&lt;strong&gt;记忆如何保存，与记忆如何参与当前决策，原来可以是两套不同的机制。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;一、先看全貌：执行历史与模型上下文为什么要分开？&lt;/h2&gt;
&lt;p&gt;先把几个模块之间的数据流放在一起：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                 Agent Loop
                     │
                     │ 每轮执行产生 Step
                     ▼
              ┌──────────────┐
              │ AgentMemory  │
              │ 结构化执行历史 │
              └──────┬───────┘
                     │
                     ▼
          write_memory_to_messages()
                     │
           ┌─────────┴─────────┐
           │                   │
           ▼                   ▼
      普通消息视图          Summary 视图
           │                   │
           ▼                   ▼
      下一轮行动            更新 Planning
           │                   │
           └─────────┬─────────┘
                     ▼
                    LLM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以借用一个后端开发的概念理解这套结构。&lt;code&gt;AgentMemory&lt;/code&gt; 像一份结构化执行日志，记录任务运行时发生了什么；&lt;code&gt;write_memory_to_messages()&lt;/code&gt; 则像面向不同请求生成 DTO 或 View 的转换层。&lt;/p&gt;
&lt;p&gt;底层保存着同一份记录，但「正常执行下一步」与「重新制定计划」，不一定需要相同的信息。这不是严格的 Event Sourcing——MemoryStep 可以修改，历史也可以重置——但它保留了一个重要思想：&lt;strong&gt;数据的存储表示，不必等于数据的消费表示。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;顺着这个区别，可以看懂三个值得细品的设计决策。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;二、决策一：为什么不直接保存 ChatMessage，而要包一层 MemoryStep？&lt;/h2&gt;
&lt;p&gt;既然模型最终接收的还是 &lt;code&gt;ChatMessage&lt;/code&gt;，为什么不直接把它们存进 &lt;code&gt;list[ChatMessage]&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;打开 &lt;code&gt;memory.py&lt;/code&gt;，&lt;code&gt;AgentMemory&lt;/code&gt; 的核心其实非常简单：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class AgentMemory:
    def __init__(self, system_prompt: str):
        self.system_prompt: SystemPromptStep = SystemPromptStep(
            system_prompt=system_prompt
        )
        self.steps: list[TaskStep | ActionStep | PlanningStep] = []
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它保存的不是已经拼好的对话消息，而是一组带有明确类型的记录。一次 &lt;code&gt;ActionStep&lt;/code&gt; 还会分别保存模型输出、工具调用、观察结果与错误：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ActionStep 关键字段节选
@dataclass
class ActionStep(MemoryStep):
    step_number: int
    timing: Timing
    tool_calls: list[ToolCall] | None = None
    error: AgentError | None = None
    model_output: str | list[dict[str, Any]] | None = None
    observations: str | None = None
    observations_images: list[&quot;PIL.Image.Image&quot;] | None = None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看上去只是多包装了一层对象，但它让系统获得了三种很实际的自由度。&lt;/p&gt;
&lt;h3&gt;字段级投影：同一段历史可以有不同的读法&lt;/h3&gt;
&lt;p&gt;如果工具调用、模型推理和执行反馈早已混成一段普通文本，那么想「只保留工具调用与 Observation，暂时不呈现模型先前的推理」，就需要重新解析文本，甚至可能恢复不了字段边界。&lt;/p&gt;
&lt;p&gt;结构化的 &lt;code&gt;ActionStep&lt;/code&gt; 不存在这个问题。比如 &lt;code&gt;ActionStep.to_messages()&lt;/code&gt; 的开头就有一条关键判断：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if self.model_output is not None and not summary_mode:
    messages.append(
        ChatMessage(
            role=MessageRole.ASSISTANT,
            content=[{&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: self.model_output.strip()}],
        )
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 &lt;code&gt;summary_mode=True&lt;/code&gt; 时，消息转换会跳过 &lt;code&gt;model_output&lt;/code&gt;。注意，&lt;strong&gt;它不是删除 Memory 里的记录，只是这一次不把这条模型输出交给模型。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;定点清理：不必把整段历史一起扔掉&lt;/h3&gt;
&lt;p&gt;Browser Agent 是一个很直观的例子。每轮浏览器操作都可能生成截图，几十轮之后，历史图片会不断占据视觉上下文。由于图片单独保存在 &lt;code&gt;observations_images&lt;/code&gt; 中，开发者可以通过 Step Callback 清理较早的截图，同时保留对应的 Tool Call 和文字 Observation。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 示意代码：可在回调中按策略清理旧截图
for step in agent.memory.steps:
    if isinstance(step, ActionStep):
        if step.step_number &amp;#x3C; current_step - 2:
            step.observations_images = None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里要区分：&lt;code&gt;to_messages()&lt;/code&gt; 的投影不修改原始记录；Callback 清理则会实际改变 Memory。它们共同依赖的能力，是执行历史具有可操作的结构，而不是不可拆分的文本块。&lt;/p&gt;
&lt;h3&gt;可观测性：一次失败不再只是一段对话&lt;/h3&gt;
&lt;p&gt;当 Agent 调用失败时，究竟是模型选错工具、参数不合法，还是工具内部执行异常？结构化记录不能自动定位根因，但至少为追踪提供了明确入口：&lt;code&gt;tool_calls&lt;/code&gt;、&lt;code&gt;observations&lt;/code&gt;、&lt;code&gt;error&lt;/code&gt;、&lt;code&gt;model_output&lt;/code&gt; 各自保存不同阶段的信息。&lt;/p&gt;
&lt;p&gt;因此，&lt;code&gt;MemoryStep&lt;/code&gt; 多出来的复杂度并不是为了让代码显得更「框架化」，而是换来&lt;strong&gt;对执行历史进行筛选、清理与分析的能力&lt;/strong&gt;。对于只运行两三轮的 Demo，直接维护消息数组也许足够；但当执行过程本身需要治理时，这层抽象就值得了。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;三、决策二：挂着 summary 名字，却没有任何摘要模型？&lt;/h2&gt;
&lt;p&gt;整个 &lt;code&gt;agents.py&lt;/code&gt; 里，我觉得最值得追下去的连接点之一，是 &lt;code&gt;write_memory_to_messages()&lt;/code&gt; 的 &lt;code&gt;summary_mode&lt;/code&gt; 参数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def write_memory_to_messages(
    self,
    summary_mode: bool = False,
) -&gt; list[ChatMessage]:
    messages = self.memory.system_prompt.to_messages(
        summary_mode=summary_mode
    )
    for memory_step in self.memory.steps:
        messages.extend(memory_step.to_messages(summary_mode=summary_mode))
    return messages
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到 Summary，很容易先想到用另一个 LLM 把长历史压缩成短摘要。但继续点进各个 Step 的 &lt;code&gt;to_messages()&lt;/code&gt;，发现这里完全没有额外的模型调用。它采用的是&lt;strong&gt;按记录类型和字段筛选消息&lt;/strong&gt;的硬规则。&lt;/p&gt;
&lt;p&gt;| 历史记录 | 普通模式 | Summary Mode |
| --- | --- | --- |
| 用户原始 Task | 保留 | 保留 |
| System Prompt | 保留 | 排除 |
| 旧 PlanningStep | 保留 | 排除 |
| ActionStep 的模型文本输出 | 保留 | 排除 |
| Tool Call | 保留 | 保留 |
| Observation / Error | 保留 | 保留 |
| Observation Images | 保留 | 仍可保留 |&lt;/p&gt;
&lt;p&gt;这个规则的取向很清晰：&lt;strong&gt;减少部分历史推理与旧计划，保留任务和实际执行轨迹。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;想象 Agent 在分析仓库时，先认定入口位于 &lt;code&gt;module_a.py&lt;/code&gt;，随后读取文件却发现真正的入口是 &lt;code&gt;module_c.py&lt;/code&gt;。前一个判断不是系统需要反复强化的事实；相比之下，「读过什么文件」「工具实际返回什么」往往更能帮助重新梳理路线。&lt;/p&gt;
&lt;p&gt;这不意味着模型输出没有价值，也不意味着 Observation 天然正确。Observation 可以来自过期文档、错误的网页或被污染的外部内容。&lt;strong&gt;它只是执行时拿到的证据，不等于经过验证的事实。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;为什么不直接做语义摘要？&lt;/h3&gt;
&lt;p&gt;LLM Summary 可以理解长历史，但也引入额外模型调用、延迟和费用，而且有遗漏关键细节、混淆事实与推测的风险。固定规则没有这样的生成过程，行为可预测，也更容易定位是哪条规则导致了信息缺失。&lt;/p&gt;
&lt;p&gt;代价同样明显：规则不懂语义，无法识别某条 Observation 是否已经过期；如果单条工具反馈本身有几万 Token，&lt;code&gt;summary_mode&lt;/code&gt; 也不会自动缩短它。所谓「低成本」，指的是&lt;strong&gt;没有额外的摘要模型调用&lt;/strong&gt;，而不是重规划不消耗 Token，更不是完全无损。&lt;/p&gt;
&lt;p&gt;这一层真正值得借鉴的地方不在布尔参数本身，而在过滤发生的位置：&lt;strong&gt;不是存储时决定某条记录以后永远有没有用，而是模型调用时根据目的决定这次呈现什么。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那么，为什么重新制定计划会需要一份与普通行动不同的上下文？&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;四、决策三：为什么 Planning 不需要复杂的 Workflow Engine？&lt;/h2&gt;
&lt;p&gt;Agent Loop 允许模型走一步、观察一步，但复杂任务只靠局部决策也容易迷路。例如研究大型仓库时，Agent 可能被某个分支文件吸引，反复追踪局部实现，却忘记最终目标是解释整体架构。&lt;/p&gt;
&lt;p&gt;smolagents 在执行循环中增加了可选的 Planning 阶段，由 &lt;code&gt;planning_interval&lt;/code&gt; 控制：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if self.planning_interval is not None and (
    self.step_number == 1
    or (self.step_number - 1) % self.planning_interval == 0
):
    for element in self._generate_planning_step(
        task,
        is_first_step=len(self.memory.steps) == 1,
        step=self.step_number,
    ):
        yield element
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如 &lt;code&gt;planning_interval=5&lt;/code&gt;，通常会在第 1、6、11……轮行动前规划。初始规划会获得任务、工具和 Managed Agents 等信息，后续再根据执行历史更新路线。&lt;/p&gt;
&lt;p&gt;但有意思的不是规划何时触发，而是&lt;strong&gt;计划生成以后，框架如何让 Agent 执行它&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Soft Planning：计划并没有编译成 DAG&lt;/h3&gt;
&lt;p&gt;查看 &lt;code&gt;PlanningStep.to_messages()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def to_messages(self, summary_mode: bool = False):
    if summary_mode:
        return []
    return [
        ChatMessage(
            role=MessageRole.ASSISTANT,
            content=[{&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: self.plan.strip()}],
        ),
        ChatMessage(
            role=MessageRole.USER,
            content=[{
                &quot;type&quot;: &quot;text&quot;,
                &quot;text&quot;: &quot;Now proceed and carry out this plan.&quot;,
            }],
        ),
    ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;框架没有把计划解析为结构化任务 DAG，也没有调度器逐项读取它。计划只是一段自然语言，作为 Assistant 消息重新进入上下文；紧接着，框架还人为构造了一条 USER 消息，要求继续执行。&lt;/p&gt;
&lt;p&gt;这条 USER 消息并非真实用户发送。源码注释明确指出，角色切换是为了防止模型接着生成计划文本。这是一个很典型的 Prompt Engineering 处理：用消息角色和内容组织，促使模型从规划回到行动。&lt;/p&gt;
&lt;p&gt;可以把它理解为 &lt;strong&gt;Soft Planning（软规划）&lt;/strong&gt;：计划提供方向，但不强制下一步必须执行哪个节点。&lt;/p&gt;
&lt;p&gt;与结构化 Workflow 相比，它的优势是灵活。探索陌生仓库时，读取第一个文件就可能推翻原有路线；固定 DAG 需要更新依赖状态，软规划则允许模型根据最新反馈直接调整行为。&lt;/p&gt;
&lt;p&gt;但如果场景是「退款前必须通过风控校验」，自然语言计划就不能当作程序级保证。严格依赖、权限和前置条件仍需要在执行层验证。smolagents 的选择并不是 Workflow Engine 的通用替代品，而是&lt;strong&gt;以更低编排复杂度换取开放式探索的灵活性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;不过它留下了更有意思的问题：既然计划会进入后续上下文，那已经过时的旧计划该怎么办？&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;五、最值得记住的一步：Replanning 为什么要主动排除旧计划？&lt;/h2&gt;
&lt;p&gt;假设 Agent 最初制定了这份计划：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. 阅读 README
2. 从 module_a.py 定位执行入口
3. 分析 module_b.py 的调用关系
4. 整理架构报告
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行到第三步时发现，&lt;code&gt;module_a.py&lt;/code&gt; 只是接口定义，真正的入口位于 &lt;code&gt;module_c.py&lt;/code&gt;。下一次更新 Planning 时，最简单的做法似乎是把旧计划和全部历史都放进 Prompt，再要求模型修订。&lt;/p&gt;
&lt;p&gt;但旧计划可能成为新的锚点：模型虽然看到了相反证据，却仍倾向于在原路线中修修补补，而不是重新评价最初的判断。&lt;/p&gt;
&lt;p&gt;smolagents 的处理方式非常直接。&lt;code&gt;_generate_planning_step()&lt;/code&gt; 的更新分支会这样构建输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;memory_messages = self.write_memory_to_messages(
    summary_mode=True
)

input_messages = (
    [plan_update_pre]
    + memory_messages
    + [plan_update_post]
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而前面 &lt;code&gt;PlanningStep.to_messages()&lt;/code&gt; 的第一条规则就是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if summary_mode:
    return []
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两段代码连起来，设计意图就很清楚了：&lt;strong&gt;旧 PlanningStep 仍然保存在 Memory 中，但这一次不会被转换成供 Planner 阅读的历史消息。&lt;/strong&gt; 任务、工具调用和 Observation 等执行信息仍然可以保留。&lt;/p&gt;
&lt;p&gt;源码注释也明确说明，排除之前的 Planning 消息，是为了避免它们过度影响新计划。这并不是凭空揣测作者动机。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                AgentMemory
                    │
          ┌─────────┴─────────┐
          │                   │
          ▼                   ▼
        旧计划              执行记录
          │                   │
      本轮不呈现         Tool / Observation
          │                   │
          └─────────┬─────────┘
                    ▼
                Replanning
                    │
                    ▼
                  新计划
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一点我很喜欢：系统没有为了重新开始就抹掉历史，也没有因为历史存在，就让旧路线始终占据模型的注意力。&lt;/p&gt;
&lt;p&gt;从架构上看，前面三个看似普通的设计终于连成一条线：&lt;code&gt;MemoryStep&lt;/code&gt; 保留可操作的执行结构，&lt;code&gt;summary_mode&lt;/code&gt; 从结构中投影出另一种上下文，Replanning 再利用这份上下文减少旧计划的影响。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「发生过」和「现在仍应作为决策依据」，是两件不同的事。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当然，这也不能彻底消除路径依赖。两条互相冲突的 Observation 仍可能同时存在，规则式筛选无法识别哪条信息更新、更可信。它解决的是一种明确的旧计划干扰，而不是完整的事实验证与知识更新。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;六、跳出源码：这套极简方案在哪里不够用？&lt;/h2&gt;
&lt;p&gt;读源码不能只顾着夸。一个设计在特定约束下足够聪明，不意味着它能直接覆盖所有生产需求。&lt;/p&gt;
&lt;h3&gt;1. 单条 Observation 过长：字段过滤救不了所有 Token&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;summary_mode&lt;/code&gt; 可以排除部分模型输出和旧计划，但不会自动压缩 Observation。如果某次网页抓取返回数万 Token，这些内容仍可能进入后续上下文。&lt;/p&gt;
&lt;p&gt;更复杂的 Agent Runtime 往往还需要工具结果截断、完整输出落盘、按需读取局部内容等机制。字段筛选与单条工具输出治理，属于两个不同层次的问题。&lt;/p&gt;
&lt;h3&gt;2. 历史证据失效：Observation 不等于权威知识&lt;/h3&gt;
&lt;p&gt;Agent 可能先读取了旧版文档，之后才找到新版；也可能在某个 Git 分支读过文件，随后仓库已切到另一个分支。Memory 忠实记录「当时返回了什么」，却无法直接保证「现在仍然成立」。&lt;/p&gt;
&lt;p&gt;因此，长期任务中还需要来源、版本、时间与重新验证机制。执行日志可以成为知识的证据来源，却不应该自动充当权威知识库。&lt;/p&gt;
&lt;h3&gt;3. Soft Planning 缺少硬性护栏&lt;/h3&gt;
&lt;p&gt;自然语言计划适合探索，不适合独自保证必须遵守的业务流程。如果某个操作需要先通过权限校验或安全检查，程序应该在执行前强制验证，而不能只依赖 Prompt 写着「先检查」。&lt;/p&gt;
&lt;p&gt;这也呼应了上一篇研究 Tool Calling 时的判断：模型负责提出行动，Runtime 负责可控地执行行动。计划再聪明，也不能替代执行边界。&lt;/p&gt;
&lt;p&gt;归根结底，&lt;code&gt;MemoryStep&lt;/code&gt; 不是长期知识库，&lt;code&gt;summary_mode&lt;/code&gt; 不是完整的语义压缩器，Soft Planning 也不是 Workflow Engine。&lt;strong&gt;值得借鉴的是它们清楚的职责划分，而不是把任何一种机制当成万能方案。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;七、写在最后&lt;/h2&gt;
&lt;p&gt;读完这部分源码，我又想起了之前一直琢磨的那个问题：如果要让一个 Agent 真正拥有某种连续的记忆，它应该记住什么？&lt;/p&gt;
&lt;p&gt;最初的想法其实挺贪心。既希望它记得过去的对话，又希望它能理解人的偏好、习惯，甚至那些没有被明确说出口，却在多次交流中逐渐显露的东西。&lt;/p&gt;
&lt;p&gt;可越想越觉得，记忆多未必是好事。&lt;/p&gt;
&lt;p&gt;人本来就是会变化的。昨天喜欢的东西，今天未必还喜欢；某次情绪下说的话，也不一定代表长期的态度。如果 Agent 把这些信息都当成稳定事实保存下来，所谓的「了解」，反而可能慢慢变成一种误解。&lt;/p&gt;
&lt;p&gt;smolagents 并没有直接回答这些问题。它处理的主要还是执行历史，而不是对人的长期理解。但它让我意识到一个很值得借鉴的区别：&lt;strong&gt;可以允许系统保留过去，却不必让过去始终拥有解释现在的权力。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;至于怎样判断一段记忆是否值得保留，什么时候应该重新验证，又该如何让旧的认识随着新的交流慢慢改变，我还没有想得特别清楚。这些问题大概也比设计一个 &lt;code&gt;Memory&lt;/code&gt; 类困难得多。&lt;/p&gt;
&lt;p&gt;下一篇继续研究 Multi-Agent 和 CodeAgent。当多个 Agent 开始协作、各自拥有不同的上下文时，信息又应该怎样传递和保持一致？&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/huggingface/smolagents&quot;&gt;Hugging Face · smolagents GitHub 仓库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://huggingface.co/docs/smolagents&quot;&gt;smolagents 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;注：文中部分代码为突出关键执行逻辑而进行的节选或简化，具体实现以源码为准。&lt;/em&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/snowfalls-next-blog-cover.D7IhNda5.png"/><enclosure url="/_astro/snowfalls-next-blog-cover.D7IhNda5.png"/></item><item><title>从 Agent Loop 到 Tool Calling：拆解 smolagents 的 Agent 执行机制</title><link>https://morrowave.me/blog/smolagents-agent-loop-tool-calling</link><guid isPermaLink="true">https://morrowave.me/blog/smolagents-agent-loop-tool-calling</guid><description>沿着 smolagents 源码，理解 Agent Loop、Tool System 与 Tool Calling 如何组成完整的 Agent 执行机制。</description><pubDate>Sun, 08 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;一个 LLM 是如何从生成文本，逐渐获得调用工具、执行行动，并根据环境反馈继续完成任务的？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;项目：&lt;/strong&gt; Hugging Face / smolagents
&lt;strong&gt;研究方向：&lt;/strong&gt; Agent Runtime · Tool System · Tool Calling
&lt;strong&gt;系列：&lt;/strong&gt; Agent 源码研究 · Part 01&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;这次沿着 smolagents 的源码，主要研究了三个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Agent Loop：&lt;/strong&gt; Agent 如何持续执行任务，并判断什么时候结束？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tool System：&lt;/strong&gt; 普通 Python 函数如何变成 LLM 可以使用的工具？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tool Calling：&lt;/strong&gt; LLM 返回工具调用请求后，Runtime 如何找到对应函数并执行？&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心结论：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent 不只是一个接入了工具的 LLM，而是一套围绕模型决策建立起来的执行系统。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;模型负责提出下一步行动，Runtime 负责将决策转化为实际执行，并将结果重新交还模型。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0. 写在前面&lt;/h2&gt;
&lt;p&gt;最近开始尝试系统地阅读 Agent 框架的源码。&lt;/p&gt;
&lt;p&gt;之前接触 Agent 时，我的注意力其实一直集中在模型能力上：模型能不能规划、能不能使用工具、能不能自主完成复杂任务。&lt;/p&gt;
&lt;p&gt;但当我真正开始尝试阅读源码时，发现自己对这些能力背后的实现机制其实并不清楚。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Agent 为什么需要一个循环？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模型究竟是怎么知道自己可以调用哪些工具的？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LLM 返回一个 Tool Call 之后，Python 又是怎么找到对应函数的？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;工具执行结果如何回到模型，影响下一步决策？&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些问题听起来并不复杂，但如果不沿着真实的代码调用链走一遍，很容易只记住几个概念。&lt;/p&gt;
&lt;p&gt;于是，我选择了 Hugging Face 开源的 smolagents 作为第一个源码研究对象。&lt;/p&gt;
&lt;p&gt;选择它主要是因为核心抽象相对集中，适合用来理解 Agent 的基本运行机制。&lt;/p&gt;
&lt;p&gt;这次阅读源码，我没有直接从第一个文件开始逐行看，而是先建立整体架构，再围绕具体问题寻找关键函数。过程中也借助 AI 帮助解释代码、梳理调用关系，但最终还是需要回到源码，确认这些解释是否成立。&lt;/p&gt;
&lt;p&gt;这篇文章记录的，就是我目前对 smolagents 核心执行机制的理解。&lt;/p&gt;
&lt;p&gt;本期主要研究 ToolCallingAgent 的执行主干，不打算把它写成一份完整的框架使用手册。重点是理解模块之间的关系，以及几个关键设计为什么存在。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 先看全貌：smolagents 的核心架构&lt;/h2&gt;
&lt;p&gt;在进入具体代码之前，先建立一张地图。&lt;/p&gt;
&lt;p&gt;smolagents 的核心 Agent 结构，可以先从以下几个部分理解：&lt;/p&gt;
&lt;p&gt;|模块|核心职责|
|---|---|
|MultiStepAgent|管理 Agent 的执行生命周期|
|ToolCallingAgent|通过模型的 Tool Calling 完成行动|
|Model|与 LLM 交互，生成下一步行动|
|Tool|定义和封装外部能力|
|Memory|保存任务执行过程中的历史记录|&lt;/p&gt;
&lt;p&gt;其中，&lt;code&gt;MultiStepAgent&lt;/code&gt; 是一个比较重要的入口。&lt;/p&gt;
&lt;p&gt;它定义了多步执行的公共机制，而 &lt;code&gt;ToolCallingAgent&lt;/code&gt; 在此基础上实现具体的工具调用策略。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;smolagents Agent
│
├─ MultiStepAgent
│   ├─ 管理执行生命周期
│   ├─ 控制 step_number
│   ├─ 判断终止条件
│   └─ 管理执行记录
│
└─ ToolCallingAgent
    ├─ 继承多步执行机制
    ├─ 调用 Model 生成下一步行动
    ├─ 解析 Tool Call
    └─ 执行工具并生成 Observation

其他核心组件
│
├─ Model       ← 负责与 LLM 交互
├─ Tool        ← 定义和封装外部能力
└─ Memory      ← 保存任务执行历史
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先不用纠结每个类的具体实现。&lt;/p&gt;
&lt;p&gt;我们先抓住一条主线：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent 接收任务 → 模型决定行动 → Runtime 执行行动 → 结果返回模型 → 继续决策。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;接下来就沿着这条主线进入源码。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. Agent Loop：整个 Agent 的执行循环&lt;/h2&gt;
&lt;p&gt;先想象一个场景。&lt;/p&gt;
&lt;p&gt;你让一个人独立去办一件事情，他不可能在出发之前就知道所有细节。走到第一个路口，他需要观察情况，再决定下一步；遇到问题，还得调整路线。&lt;/p&gt;
&lt;p&gt;Agent 其实也有点类似。&lt;/p&gt;
&lt;p&gt;它不是提前写好一条固定路线，而是每完成一步，就根据新的信息决定下一步。&lt;/p&gt;
&lt;h3&gt;2.1 MultiStepAgent：为什么需要循环？&lt;/h3&gt;
&lt;p&gt;我们先看 &lt;code&gt;agents.py&lt;/code&gt; 中的 &lt;code&gt;_run_stream()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def _run_stream(self, task, max_steps, images=None):
    self.step_number = 1
    returned_final_answer = False

    while (
        not returned_final_answer
        and self.step_number &amp;#x3C;= max_steps
    ):
        # 创建当前行动步骤
        action_step = ActionStep(
            step_number=self.step_number,
            ...
        )

        # 执行当前步骤
        for output in self._step_stream(action_step):
            yield output

            # 收到最终答案后，结束循环
            if isinstance(output, ActionOutput) and output.is_final_answer:
                ...
                returned_final_answer = True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;注：保留关键执行逻辑，省略部分异常处理和辅助代码。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这里最重要的就是 &lt;code&gt;while&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;普通 LLM 调用通常是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;输入 → 模型推理 → 输出 → 结束
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Agent 则是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;输入
 │
 ▼
模型决策
 │
 ▼
执行行动
 │
 ▼
获取结果
 │
 ▼
再次决策
 │
 └──────────↺
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每一步都可能产生新的信息，而这些信息又会影响下一步决策。&lt;/p&gt;
&lt;p&gt;因此，Agent 需要一个持续运行的循环。&lt;/p&gt;
&lt;h3&gt;2.2 两个终止条件：什么时候停？&lt;/h3&gt;
&lt;p&gt;回到源码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;while (
    not returned_final_answer
    and self.step_number &amp;#x3C;= max_steps
):
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有两个重要条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;returned_final_answer&lt;/code&gt;：是否已经获得最终答案。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;max_steps&lt;/code&gt;：最多允许执行多少步。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么需要两个条件？&lt;/p&gt;
&lt;p&gt;如果模型一直没有决定结束任务，只依赖最终答案判断，Agent 可能持续运行。&lt;/p&gt;
&lt;p&gt;而如果只依赖最大步数，即使任务提前完成，也没有必要继续执行。&lt;/p&gt;
&lt;p&gt;所以两者共同构成了 Agent Loop 的基本终止机制。&lt;/p&gt;
&lt;p&gt;此外，源码还提供了 &lt;code&gt;interrupt_switch&lt;/code&gt;，用于外部中断。&lt;/p&gt;
&lt;p&gt;如果达到最大步数仍然没有得到最终答案，框架会调用 &lt;code&gt;_handle_max_steps_reached()&lt;/code&gt;，尝试生成一个收尾答案。&lt;/p&gt;
&lt;p&gt;这里还有一个值得注意的区别：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最大步数限制的是执行步骤，不等于 Token 预算、API 请求次数或运行时间限制。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;2.3 ActionStep：每一轮行动如何记录？&lt;/h3&gt;
&lt;p&gt;继续看：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;action_step = ActionStep(
    step_number=self.step_number,
    timing=Timing(start_time=action_step_start_time),
    observations_images=images,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次进入循环，框架都会创建一个新的 &lt;code&gt;ActionStep&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;它负责记录当前步骤的执行信息，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;模型输入和输出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Token 使用情况。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;工具调用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Observation。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;错误信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是否为最终答案。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以简单理解为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ActionStep 1
├─ 模型输入
├─ 模型输出
├─ 工具调用
└─ 执行结果

ActionStep 2
├─ 模型输入
├─ 模型输出
├─ 工具调用
└─ 执行结果
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我一开始也容易把 ActionStep 和行动编排混淆。&lt;/p&gt;
&lt;p&gt;但实际上，它主要负责记录，而不是决定下一步调用哪个工具。&lt;/p&gt;
&lt;p&gt;真正执行当前一步的是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;for output in self._step_stream(action_step):
    yield output
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;_run_stream()&lt;/code&gt; 管理整个生命周期。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;_step_stream()&lt;/code&gt; 执行当前步骤。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ActionStep&lt;/code&gt; 记录当前步骤。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这三个职责需要区分。&lt;/p&gt;
&lt;h3&gt;2.4 谁决定任务完成？&lt;/h3&gt;
&lt;p&gt;在 ToolCallingAgent 中，模型可以通过特殊工具 &lt;code&gt;final_answer&lt;/code&gt; 提交最终答案。&lt;/p&gt;
&lt;p&gt;框架通过工具名称识别：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;is_final_answer = tool_name == &quot;final_answer&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当它被识别为最终答案后，工具结果会被标记为 &lt;code&gt;is_final_answer=True&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;随后 &lt;code&gt;_step_stream()&lt;/code&gt; 产生 &lt;code&gt;ActionOutput&lt;/code&gt;，交回外层 &lt;code&gt;_run_stream()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;外层循环识别到最终答案后，将：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;returned_final_answer = True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是循环结束。&lt;/p&gt;
&lt;p&gt;这里有一个值得注意的设计点：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模型决定什么时候提交最终答案，但程序决定如何识别、校验和结束执行。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而且，提交最终答案不代表任务一定成功。&lt;/p&gt;
&lt;p&gt;例如，用户要求整理五篇论文，模型只找到两篇就提交答案。&lt;/p&gt;
&lt;p&gt;Agent 可以正常结束，但任务显然没有完全完成。&lt;/p&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Termination：执行是否结束。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Success：任务是否真正完成。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两件事不能混为一谈。&lt;/p&gt;
&lt;p&gt;Agent Loop 并不负责决定具体行动，而是提供一个持续执行的生命周期。具体行动由 Agent 子类实现，执行记录与生命周期控制也被分离。这种职责划分使不同类型的 Agent 能够复用相同的执行框架。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. Tool System：Agent 如何获得外部能力？&lt;/h2&gt;
&lt;p&gt;如果把 Agent 比作一个能够独立完成任务的人，那么 Tool 就像他可以使用的工具箱。&lt;/p&gt;
&lt;p&gt;但工具箱里只有工具还不够。&lt;/p&gt;
&lt;p&gt;他还需要知道每件工具叫什么、能做什么、应该怎么使用。&lt;/p&gt;
&lt;p&gt;Tool System 解决的就是这个问题。&lt;/p&gt;
&lt;h3&gt;3.1 BaseTool 与 Tool&lt;/h3&gt;
&lt;p&gt;smolagents 在 &lt;code&gt;tools.py&lt;/code&gt; 中通过 &lt;code&gt;BaseTool&lt;/code&gt; 和 &lt;code&gt;Tool&lt;/code&gt; 建立统一的工具抽象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class BaseTool(ABC):
    name: str

    @abstractmethod
    def __call__(self, *args, **kwargs):
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里使用了 Python 的抽象基类机制。&lt;/p&gt;
&lt;p&gt;它规定了具体工具需要实现统一的调用入口。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;Tool&lt;/code&gt; 在此基础上增加了一套工具元数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Tool(BaseTool):
    name: str
    description: str
    inputs: dict
    output_type: str
    output_schema: dict | None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些属性分别描述：&lt;/p&gt;
&lt;p&gt;|属性|含义|
|---|---|
|&lt;code&gt;name&lt;/code&gt;|工具名称|
|&lt;code&gt;description&lt;/code&gt;|工具功能描述|
|&lt;code&gt;inputs&lt;/code&gt;|输入参数规范|
|&lt;code&gt;output_type&lt;/code&gt;|输出类型|
|&lt;code&gt;output_schema&lt;/code&gt;|可选的结构化输出规范|&lt;/p&gt;
&lt;p&gt;真正实现工具功能的是 &lt;code&gt;forward()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class WeatherTool(Tool):
    name = &quot;weather&quot;
    description = &quot;查询指定城市天气&quot;

    inputs = {
        &quot;city&quot;: {
            &quot;type&quot;: &quot;string&quot;,
            &quot;description&quot;: &quot;城市名称&quot;
        }
    }

    output_type = &quot;string&quot;

    def forward(self, city: str):
        # 这里才是真正的业务逻辑
        return f&quot;{city}：晴天，25℃&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，工具的功能描述与实际执行逻辑就被封装在同一个对象中。&lt;/p&gt;
&lt;h3&gt;3.2 &lt;code&gt;__call__()&lt;/code&gt; 和 &lt;code&gt;forward()&lt;/code&gt; 有什么区别？&lt;/h3&gt;
&lt;p&gt;看 &lt;code&gt;Tool.__call__()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def __call__(
    self,
    *args,
    sanitize_inputs_outputs=False,
    **kwargs
):
    # 首次使用时执行初始化
    if not self.is_initialized:
        self.setup()

    # 处理输入
    if sanitize_inputs_outputs:
        args, kwargs = handle_agent_input_types(*args, **kwargs)

    # 执行真正的工具逻辑
    outputs = self.forward(*args, **kwargs)

    return outputs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;__call__&lt;/code&gt; 是 Python 的特殊方法，它让对象可以像函数一样被调用。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;weather = WeatherTool()

# 看起来像普通函数调用
result = weather(city=&quot;宁波&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际执行过程是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;weather(...)
    │
    ▼
Tool.__call__()
    │
    ├─ 检查初始化
    ├─ 处理输入
    ▼
Tool.forward()
    │
    ▼
返回结果
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里还有一个小细节：&lt;code&gt;setup()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;某些工具可能需要加载大型模型，或者执行比较昂贵的初始化操作。&lt;/p&gt;
&lt;p&gt;smolagents 允许在第一次调用工具时再执行初始化，这就是延迟初始化（Lazy Initialization）。&lt;/p&gt;
&lt;h3&gt;3.3 &lt;code&gt;@tool&lt;/code&gt;：普通函数也可以变成 Tool&lt;/h3&gt;
&lt;p&gt;如果每个简单工具都需要继承 &lt;code&gt;Tool&lt;/code&gt;，填写属性，再实现 &lt;code&gt;forward()&lt;/code&gt;，就会有不少样板代码。&lt;/p&gt;
&lt;p&gt;所以 smolagents 还提供了 &lt;code&gt;@tool&lt;/code&gt; 装饰器。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@tool
def add(a: int, b: int) -&gt; int:
    &quot;&quot;&quot;
    计算两个整数的和。

    Args:
        a: 第一个整数
        b: 第二个整数
    &quot;&quot;&quot;
    return a + b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会把普通 Python 函数转换成一个动态创建的 Tool 子类实例。&lt;/p&gt;
&lt;p&gt;整个过程可以概括为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;普通 Python 函数
    │
    ▼
读取函数信息
    │
    ▼
生成工具 Schema
    │
    ▼
创建 Tool 子类
    │
    ▼
绑定原函数到 forward()
    │
    ▼
返回 Tool 实例
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里不展开装饰器内部的动态类生成细节。&lt;/p&gt;
&lt;p&gt;对理解 Agent Runtime 来说，先记住一点就够了：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;**@tool**&lt;/code&gt; &lt;strong&gt;让开发者可以用普通函数的形式定义工具，而框架负责将其转换成统一的 Tool 对象。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;3.4 JSON Schema：模型怎么知道工具需要什么参数？&lt;/h3&gt;
&lt;p&gt;假设我们定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def search(query: str, limit: int):
    &quot;&quot;&quot;
    搜索互联网信息。

    Args:
        query: 搜索关键词
        limit: 返回结果数量
    &quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;框架可以通过类型注解和 Docstring 提取：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;工具名称和用途。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参数名称。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参数类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参数描述。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后生成结构化的 JSON Schema。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;name&quot;: &quot;search&quot;,
  &quot;description&quot;: &quot;搜索互联网信息&quot;,
  &quot;parameters&quot;: {
    &quot;properties&quot;: {
      &quot;query&quot;: {
        &quot;type&quot;: &quot;string&quot;,
        &quot;description&quot;: &quot;搜索关键词&quot;
      },
      &quot;limit&quot;: {
        &quot;type&quot;: &quot;integer&quot;,
        &quot;description&quot;: &quot;返回结果数量&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;注：这是帮助理解的简化示意结构，并非完整的源码输出。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这就解决了一个问题：&lt;/p&gt;
&lt;p&gt;模型不需要知道 Python 函数内部怎么实现，只需要知道工具叫什么、能做什么、需要什么参数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JSON Schema 是模型与程序之间的调用契约。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它负责描述工具，不负责执行工具。&lt;/p&gt;
&lt;h3&gt;3.5 参数校验：不能完全相信模型&lt;/h3&gt;
&lt;p&gt;模型生成的参数不一定总是正确的。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;query&quot;: 123,
    &quot;limit&quot;: &quot;很多&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但工具实际要求：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;query&quot;: &quot;string&quot;,
    &quot;limit&quot;: &quot;integer&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，smolagents 还提供了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;validate_tool_arguments(tool, arguments)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会检查参数是否存在于工具定义中、必要参数是否缺失，以及实际类型是否符合要求。&lt;/p&gt;
&lt;p&gt;这里需要区分两个层面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;工具定义时的校验：检查 Tool 自身的描述和输入规范。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;工具调用时的校验：检查模型实际传入的参数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前者保证工具定义合理，后者保证一次具体调用符合要求。&lt;/p&gt;
&lt;p&gt;Tool System 的关键不只是封装函数，而是建立一份模型能够理解、Runtime 能够执行的统一接口规范。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. Tool Calling：从模型决策到程序执行&lt;/h2&gt;
&lt;p&gt;前面解决了两个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Agent 如何持续运行？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;工具如何被定义？&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在到了最关键的部分。&lt;/p&gt;
&lt;p&gt;可以把这一过程想象成一个任务调度中心：模型提出任务请求，Runtime 拿到请求后，确认目标工具是否存在、参数是否合理，再把任务交给真正负责执行的程序，最后将结果送回来。&lt;/p&gt;
&lt;h3&gt;4.1 模型如何获得工具清单？&lt;/h3&gt;
&lt;p&gt;回到 &lt;code&gt;agents.py&lt;/code&gt; 中的 &lt;code&gt;ToolCallingAgent._step_stream()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;首先：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 将 Agent 历史记录转换为模型消息
memory_messages = self.write_memory_to_messages()

input_messages = memory_messages.copy()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来调用模型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;chat_message = self.model.generate(
    input_messages,
    stop_sequences=[&quot;Observation:&quot;, &quot;Calling tools:&quot;],
    tools_to_call_from=self.tools_and_managed_agents,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有两个重要输入：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;input_messages&lt;/code&gt;：当前任务和历史上下文。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tools_to_call_from&lt;/code&gt;：当前 Agent 可以使用的工具定义。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;模型根据这些信息决定下一步行动。&lt;/p&gt;
&lt;p&gt;它可以直接生成文本，也可以提出工具调用请求。&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模型返回 Tool Call，只是提出行动请求，并不代表工具已经执行。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;4.2 Tool Call：统一模型输出&lt;/h3&gt;
&lt;p&gt;不同模型供应商的工具调用格式可能存在差异。&lt;/p&gt;
&lt;p&gt;smolagents 会将模型输出转换成内部的 &lt;code&gt;ToolCall&lt;/code&gt; 对象：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;tool_call = ToolCall(
    name=chat_tool_call.function.name,
    arguments=chat_tool_call.function.arguments,
    id=chat_tool_call.id
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样后续 Runtime 就可以使用统一的数据结构，而不必直接依赖某个模型供应商的原始格式。&lt;/p&gt;
&lt;h3&gt;4.3 Runtime 怎么找到真正的工具？&lt;/h3&gt;
&lt;p&gt;假设模型返回：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;name&quot;: &quot;search&quot;,
  &quot;arguments&quot;: {
    &quot;query&quot;: &quot;smolagents&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python 怎么知道应该执行哪个函数？&lt;/p&gt;
&lt;p&gt;我们来看 &lt;code&gt;execute_tool_call()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 普通工具 + 被管理的 Agent
available_tools = {
    **self.tools,
    **self.managed_agents
}

# 根据名称找到实际工具对象
tool = available_tools[tool_name]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里其实就是一个名称到对象的映射。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;self.tools = {
    &quot;search&quot;: search_tool,
    &quot;calculator&quot;: calculator_tool,
    &quot;weather&quot;: weather_tool
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模型返回：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;tool_name = &quot;search&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Runtime 就可以通过：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;tool = self.tools[&quot;search&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找到真正的工具对象。&lt;/p&gt;
&lt;p&gt;这里可以将这套映射理解为 Tool Registry（工具注册表）。&lt;/p&gt;
&lt;p&gt;需要说明的是，这是对源码中映射结构的架构概括，并不是源码中独立存在的一个 Registry 类。&lt;/p&gt;
&lt;p&gt;另外，&lt;code&gt;available_tools&lt;/code&gt; 还合并了 &lt;code&gt;managed_agents&lt;/code&gt;，所以被管理的其他 Agent 也可以作为当前 Agent 的可调用对象。&lt;/p&gt;
&lt;h3&gt;4.4 参数替换与校验&lt;/h3&gt;
&lt;p&gt;找到工具之后，还不能直接执行。&lt;/p&gt;
&lt;p&gt;源码接下来会做：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 将参数中引用的状态变量替换成实际对象
arguments = self._substitute_state_variables(arguments)

# 判断是不是 Managed Agent
is_managed_agent = tool_name in self.managed_agents

# 校验实际调用参数
validate_tool_arguments(tool, arguments)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的状态变量替换值得注意。&lt;/p&gt;
&lt;p&gt;例如 Agent State 中保存了一个对象：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;self.state = {
    &quot;image.png&quot;: image_object
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果模型传入的参数中包含：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&quot;image&quot;: &quot;image.png&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Runtime 就可以将这个字符串引用替换成实际保存的对象。&lt;/p&gt;
&lt;p&gt;这样，模型就不必每次都重新传递完整对象。&lt;/p&gt;
&lt;p&gt;随后，框架会对参数进行校验。&lt;/p&gt;
&lt;p&gt;如果参数不符合工具规范，会抛出相应的调用错误，而不是直接执行。&lt;/p&gt;
&lt;h3&gt;4.5 真正执行：&lt;code&gt;tool(**arguments)&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;终于到这里了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 普通 Tool
return tool(
    **arguments,
    sanitize_inputs_outputs=True
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;**arguments&lt;/code&gt; 是 Python 的字典解包。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;arguments = {
    &quot;query&quot;: &quot;smolagents&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;tool(**arguments)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就等价于：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;tool(query=&quot;smolagents&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终会进入之前研究的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Tool.__call__()
    │
    ▼
Tool.forward()
    │
    ▼
真正的工具实现
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，模型生成的结构化行动请求就被转换成了真实的 Python 函数调用。&lt;/p&gt;
&lt;h3&gt;4.6 Observation：工具执行结果如何返回？&lt;/h3&gt;
&lt;p&gt;工具执行完毕之后，smolagents 会将结果封装成 &lt;code&gt;ToolOutput&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;return ToolOutput(
    id=tool_call.id,
    output=tool_call_result,
    is_final_answer=is_final_answer,
    observation=observation,
    tool_call=tool_call,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有两个值得区分的字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;output&lt;/code&gt;：工具实际返回的对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;observation&lt;/code&gt;：提供给 Agent 后续处理的观察结果。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于普通文本结果，框架会将它转换为字符串形式的 Observation。&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;AgentImage&lt;/code&gt;、&lt;code&gt;AgentAudio&lt;/code&gt; 等特定对象，则会保存到 Agent State，并通过引用名称生成观察描述。&lt;/p&gt;
&lt;p&gt;随后，工具调用和 Observation 会被写入当前 &lt;code&gt;ActionStep&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;外层 &lt;code&gt;_run_stream()&lt;/code&gt; 完成这一步之后，再将 &lt;code&gt;ActionStep&lt;/code&gt; 加入 Memory。&lt;/p&gt;
&lt;p&gt;下一轮执行时，之前的执行历史又会重新进入模型上下文。&lt;/p&gt;
&lt;p&gt;这样就形成了完整闭环：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;模型提出行动请求
    │
    ▼
解析为 ToolCall
    │
    ▼
Runtime 查找工具
    │
    ▼
替换状态变量、校验参数
    │
    ▼
调用 tool(**arguments)
    │
    ▼
Tool.__call__() → Tool.forward()
    │
    ▼
生成 ToolOutput
    │
    ▼
记录 ActionStep / Observation
    │
    ▼
进入下一轮模型决策
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这才是 Tool Calling 真正有意思的地方。&lt;/p&gt;
&lt;p&gt;它不是一次独立的函数调用，而是 Agent 多步执行机制中的一个环节。&lt;/p&gt;
&lt;h3&gt;4.7 一个额外的细节：并行工具调用&lt;/h3&gt;
&lt;p&gt;继续往下看 &lt;code&gt;process_tool_calls()&lt;/code&gt;，还能发现一个有意思的设计。&lt;/p&gt;
&lt;p&gt;当模型一次只提出一个 Tool Call 时，框架直接执行。&lt;/p&gt;
&lt;p&gt;但如果一次提出多个调用，就会使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;ThreadPoolExecutor(self.max_tool_threads)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过线程池并行执行。&lt;/p&gt;
&lt;p&gt;例如，模型认为两个查询互不依赖：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查询项目许可证。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询项目最新版本。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;就可以在同一轮提出两个工具调用，让 Runtime 并行处理。&lt;/p&gt;
&lt;p&gt;这里需要注意：并行执行需要模型在同一轮提出多个 Tool Call，并不是 Runtime 自动分析任务后决定并行。&lt;/p&gt;
&lt;p&gt;这一机制实际上把两个问题分开了：模型负责决定哪些行动可以同时提出，Runtime 负责提供并行执行能力。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 把几个模块串起来：一次完整的 Agent 执行&lt;/h2&gt;
&lt;p&gt;前面分别研究了 Agent Loop、Tool System 和 Tool Calling。&lt;/p&gt;
&lt;p&gt;现在把它们放在一起。&lt;/p&gt;
&lt;p&gt;假设用户要求：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;找到 smolagents 项目，阅读 README，并总结它的核心功能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一次典型的多步执行可以这样理解：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;用户提出任务
│
├─ 1. Runtime 组装输入
│   ├─ 当前任务
│   ├─ 执行历史（Memory）
│   └─ 可用工具清单
│
├─ 2. LLM 决定下一步行动
│   ├─ 可能直接回答
│   └─ 也可能发起 Tool Call
│
├─ 3. 如果发起 Tool Call
│   ├─ Runtime 查找对应工具
│   ├─ 校验参数是否合法
│   └─ 执行工具
│
├─ 4. 工具返回结果
│   └─ 结果会被整理成 Observation
│
├─ 5. Observation 写回执行历史
│   └─ 供下一轮 LLM 决策使用
│
├─ 6. LLM 再次判断
│   ├─ 继续调用工具
│   └─ 或提交 final_answer
│
└─ 7. Runtime 返回最终结果
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里最重要的是：第 6 步并不是一个固定的后续流程。&lt;/p&gt;
&lt;p&gt;模型可以根据工具返回的信息继续调用其他工具，也可以认为现有信息已经足够，直接提交最终答案。&lt;/p&gt;
&lt;p&gt;因此，这是一套动态决策机制，而不是预先写死的工作流。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 几个值得进一步思考的设计问题&lt;/h2&gt;
&lt;p&gt;读源码不应该只停留在知道代码做了什么，还需要继续问：为什么这样设计？&lt;/p&gt;
&lt;h3&gt;6.1 模型越来越强，还需要 Runtime 吗？&lt;/h3&gt;
&lt;p&gt;我觉得需要先区分固定 Workflow 和自主 Agent。&lt;/p&gt;
&lt;p&gt;固定 Workflow：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;data = search(query)
result = analyze(data)
answer = summarize(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每一步执行顺序由程序预先规定。&lt;/p&gt;
&lt;p&gt;自主 Agent：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;while not finished:
    action = llm.decide(memory)
    result = execute(action)
    memory.append(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下一步行动由模型动态决定。&lt;/p&gt;
&lt;p&gt;这并不意味着程序不再需要控制。&lt;/p&gt;
&lt;p&gt;即使模型足够强，系统仍然需要执行步数限制、工具权限、参数校验、中断和异常处理。&lt;/p&gt;
&lt;p&gt;我的理解是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模型能力决定 Agent 能做出多灵活的决策，Runtime 决定这些决策能否在可控的环境中执行。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;6.2 为什么不直接把所有工具都交给模型？&lt;/h3&gt;
&lt;p&gt;随着工具数量增加，模型需要面对越来越大的选择空间。&lt;/p&gt;
&lt;p&gt;工具描述和参数 Schema 也会占用上下文。&lt;/p&gt;
&lt;p&gt;可能出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;上下文负担增加。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;功能相似的工具容易混淆。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无关工具干扰决策。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;工具管理与权限控制更加复杂。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，工具数量并不是越多越好。&lt;/p&gt;
&lt;p&gt;对于工具规模更大的系统，可以进一步考虑工具发现、动态筛选和分层路由。&lt;/p&gt;
&lt;h3&gt;6.3 Termination 不等于 Success&lt;/h3&gt;
&lt;p&gt;模型提交 &lt;code&gt;final_answer&lt;/code&gt;，只能说明它决定结束行动。&lt;/p&gt;
&lt;p&gt;这并不自动证明用户要求已经被满足。&lt;/p&gt;
&lt;p&gt;一个能够停止运行的 Agent，不一定是一个能够可靠完成任务的 Agent。&lt;/p&gt;
&lt;p&gt;因此，Agent 系统还需要考虑任务验收、结果验证和失败处理。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 写在最后&lt;/h2&gt;
&lt;p&gt;这次研究让我对 Agent 的内部运行机制有了更具体的认识。&lt;/p&gt;
&lt;p&gt;沿着 smolagents 的源码走下来，可以发现，Agent 的执行过程并不是模型单方面完成的。&lt;/p&gt;
&lt;p&gt;从 &lt;code&gt;MultiStepAgent&lt;/code&gt; 的执行循环，到 &lt;code&gt;Tool&lt;/code&gt; 的统一抽象，再到 &lt;code&gt;execute_tool_call()&lt;/code&gt; 将模型输出转化为真实的 Python 函数调用，每个模块都承担着相对明确的职责。&lt;/p&gt;
&lt;p&gt;其中一个值得关注的设计是：模型的决策能力与程序的执行能力被分离了。&lt;/p&gt;
&lt;p&gt;模型可以灵活地提出行动，但实际行动仍然需要经过 Runtime 的查找、校验和执行。工具产生的结果又会被重新组织为模型可以理解的信息，进入下一轮决策。&lt;/p&gt;
&lt;p&gt;这套机制构成了 Agent 能够持续与外部环境交互的基础。&lt;/p&gt;
&lt;p&gt;当然，这还只是 smolagents 的一部分。&lt;/p&gt;
&lt;p&gt;目前主要研究了 ToolCallingAgent 的执行主干，Memory 的内部结构、上下文管理以及 CodeAgent 的代码执行机制还没有深入展开。&lt;/p&gt;
&lt;h3&gt;后续更新计划&lt;/h3&gt;
&lt;p&gt;接下来准备继续沿着几个方向研究。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Part 02：Memory &amp;#x26; Context&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;AgentMemory 的内部结构。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TaskStep、ActionStep 等执行记录。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Observation 如何进入 Memory。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;write_memory_to_messages()&lt;/code&gt; 如何重建模型上下文。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多轮执行如何保持连续性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Part 03：CodeAgent &amp;#x26; Executor&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;CodeAgent 与 ToolCallingAgent 的区别。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模型生成代码的执行流程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Python Interpreter 的实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行环境与安全边界。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Part 04：扩展机制与架构设计&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Managed Agents。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MCP 工具接入。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Planning 机制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Streaming 与事件处理。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;smolagents 的架构取舍。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后面还想单独做一次完整的架构复盘，把前面分散研究的模块重新连接起来。&lt;/p&gt;
&lt;p&gt;这次先到这里。&lt;/p&gt;
&lt;p&gt;从 Agent 如何行动开始，再逐渐理解它如何记忆、执行和扩展。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/huggingface/smolagents&quot;&gt;Hugging Face · smolagents GitHub 仓库&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://huggingface.co/docs/smolagents&quot;&gt;smolagents 官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/smolagents-agent-loop-tool-calling-cover.BvFtC4DF.png"/><enclosure url="/_astro/smolagents-agent-loop-tool-calling-cover.BvFtC4DF.png"/></item></channel></rss>