Agent 类栈:从纯 LLM 到智能编码 agent 的继承链
30 秒导读: fast-agent 里所有能对话、能调工具、能连 MCP、能跑
/slash命令的 agent,都不是各写一套,而是一条单继承链从下往上一层层叠出来的。本章讲清这条链每一层「在上一层基础上补了什么能力」,给你一张继承图和一张对照表。
本章是 [Agent 类栈] 这一章,属于 fast-agent 文档组。它只讲类之间的继承与分工;工具循环一次 turn 内部怎么跑留给 03-tool-loop-engine,provider 细节留给 04-llm-provider-abstraction,MCP 聚合器内部留给 05-mcp-integration。想先看 agent 是怎么被声明出来的,回 01-declarative-frontend。
1. 这是什么(零基础也能懂)
一句话定义: 这是 fast-agent 里 agent 对象的「能力继承阶梯」——从一个只会把消息转发给模型的空壳,一路继承、加料,直到一个能在终端里帮你改代码、连外部工具、跑命令的智能编码 agent。
它解决什么问题: 一个 agent 框架要支持很多形态——最简单的「纯问答」、能调本地函数的、能连 MCP 服务器的、带完整交互式编码体验的。如果每种形态都从零写一套,代码会重复且容易走样。fast-agent 的选择是:把能力切成一层一层,每层是上一层的子类,只负责补一件新事。你要哪种 agent,就用到链上哪一层。
用起来什么样: 你几乎从不直接 new 这些类(声明式前端替你选类,见 01 章),但理解这条链能让你看懂「为什么一个 SmartAgent 既能对话、又能调工具、又能连 MCP」——因为这些能力分别来自它继承链上的不同祖先。
一句话直觉/类比: 把它想成咖啡的加料:先是一杯 espresso(只会调模型),加奶变拿铁(能显示、能流式),加糖浆(能调工具),再加一份浓缩(接上 MCP 运行时),最后拉花装盘(交互命令)。每一步都基于上一步,不推翻它。
2. 顶层全景(这条链大概怎么转)
整条链是单继承(每层只有一个直接父类,外加少量 mixin/Protocol),从抽象到具体是这样的:
怎么读这张图:从上到下是「越来越能干」,每个框第二行是它新增的核心能力,括号里是真实类所在文件。
┌─────────────────────────────────────────────────────────┐
│ ① LlmDecorator llm_decorator.py │
│ 门面:__call__/send/generate、attach_llm、历史与生命周期 │
└───────────────────────────┬─────────────────────────────┘
│ 继承
┌───────────────────────────▼─────────────────────────────┐
│ ② LlmAgent llm_agent.py │
│ 加:UI 显示、流式、停止原因、取消回滚 │
└───────────────────────────┬─────────────────────────────┘
│ 继承 (+ _ToolLoopAgent mixin)
┌───────────────────────────▼─────────────────────────────┐
│ ③ ToolAgent tool_agent.py │
│ 加:本地函数工具、并行/串行执行、agent-as-tool │
└───────────────────────────┬─────────────────────────────┘
│ 继承 (名义挂 ABC,但无抽象方法)
┌───────────────────────────▼─────────────────────────────┐
│ ④ McpAgent mcp_agent.py │
│ 加:MCP 聚合器、shell/文件系统运行时、skills、指令模板 │
└───────────────────────────┬─────────────────────────────┘
│ 继承
┌───────────────────────────▼─────────────────────────────┐
│ ⑤ SmartAgent / SmartAgentWithUI smart_agent.py │
│ 加:/slash 命令、/connect、交互式编码 agent 体验 │
└─────────────────────────────────────────────────────────┘
每层一句话职责:
| 层 | 类 | 文件 | 补了什么(一句话) |
|---|---|---|---|
| ① | LlmDecorator | agents/llm_decorator.py:167 | 对外门面 + 挂载 LLM + 会话历史,但自己不加任何 LLM 交互行为 |
| ② | LlmAgent | agents/llm_agent.py:109 | 把一次回复显示出来:流式渲染、停止原因文案、取消回滚 |
| ③ | ToolAgent | agents/tool_agent.py:50 | 让 agent会用工具:注册本地函数、并行/串行跑、把别的 agent 当工具 |
| ④ | McpAgent | agents/mcp_agent.py:178 | 接上外部世界:MCP 服务器、shell、文件系统、skills、指令模板渲染;它就是「普通 agent」(AgentType.BASIC)本体 |
| ⑤ | SmartAgent | agents/smart_agent.py:1870 | 交互式编码 agent 体验:/slash 命令、运行时 /mcp connect、smart 工具 |
主线走一遍(高层): 你调 agent("帮我改这个文件") → 无论 agent 是哪一层,入口都是 ① 的 __call__;它一路 send → generate → generate_impl。generate_impl 是被每层逐层重 写的那个方法——① 只转发给 LLM,② 包上显示,③ 包上工具循环,④ 把工具循环里的工具换成 MCP 工具。所以「同一个调用,行为随 agent 具体是哪一层而层层加厚」。
3. 核心机制:逐层看「补了什么」
下面每一小节对应链上一层,统一回答三件事:它解决的小问题 → 关键设计 → 真实源码锚点。
3.1 ① LlmDecorator —— 门面 + 挂载点,自己不干活
它要解决的小问题: 需要一个「所有 agent 都长一个样」的对外接口,同时把「真正调模型」这件事解耦出去,方便换 provider、方便子类插入行为。
关键设计一:统一门面。 对外只暴露少数几个入口,并且它们是层层收窄的:__call__ 直接转 send,send 调 generate 再取文本,generate 归一化输入后调 generate_impl。
# 示意,非源码:门面的三级转发
async def __call__(self, message): # 最外层:让 agent 可直接调用
return await self.send(message)
async def send(self, message, params=None):
resp = await self.generate(message, params) # 拿到完整回复对象
return resp.last_text() or "" # 只取最后一段文本
真实实现见 agents/llm_decorator.py:508(__call__)、:526(send)、:540(generate)。重点看 generate_impl(:576):基类版本只是把消息交给挂着的 LLM(_generate_with_summary),不加任何花样——花样全靠子类重写这一个方法。
关键设计二:LLM 是「挂上去」的,不是构造进来的。 agent 先被创建(此时 _llm = None),之后调 attach_llm 才把一个具体 provider 的 LLM 实例装上(agents/llm_decorator.py:394 attach_llm)。挂载时用三级优先级合并请求参数(方法内 _merge_request_params,:1388),并把工厂和 kwargs 存下来,供之后 set_model 换模型或 spawn_detached_instance 克隆时复用。
关键设计三:会话历史归 agent 自己管。 历史存在 _message_history(全部是 PromptMessageExtended),generate 前由 _prepare_llm_call(:831)拼「模板前缀 + 历史 + 本轮」,回复后由 _persist_history(:894)落库。这里还藏着一个精华:送给模型前会剔掉当前模型不支持的内容块(_sanitize_messages_for_llm,:927),把被删的图片/文档记进 error 通道并给出摘要,避免不支持 vision 的模型收到图片直接报错。
关键设计四:spawn_detached_instance——克隆出一个独立实例。 它 deep-copy 配置、新建同类型对象、重新 attach_llm,得到一个有自己 MCP/LLM 栈的全新 agent(agents/llm_decorator.py:448)。这是 3.3 里「agent 当工具」能并发、能隔离状态的底座。
关键设计五:流式监听靠 mixin 转发。 LlmDecorator 同时混入 StreamingAgentMixin(:84),把 add_stream_listener 等注册转发给挂着的 LLM;没挂 LLM 时返回一个空的取消函数,安全降级。
一句话:① 层是「接口 + 状态容器 + 挂载点」,它定义了
generate_impl这个给子类改写的空位,自己填的是最朴素的实现。
3.2 ② LlmAgent —— 把一次回复「显示」出来
它要解决的小问题: 光能拿到模型回复还不够,终端里得看得见——要流式打字机效果、要在被截断/被安全过滤/被取消时给出对应提示、要能在用户中断时回滚历史。
关键设计:重写 generate_impl,包一层显示。 LlmAgent.generate_impl(agents/llm_agent.py:813)先决定要不要流式(_should_stream,:762),流式就开一个 streaming 显示句柄、把 LLM 的 chunk 喂给它,拿到结果后再 show_assistant_message 定稿;不流式就直接生成再显示。
停止原因被翻译成人话文案。STOP_REASON_ADDITIONAL_MESSAGES(agents/llm_agent.py:77)把 MAX_TOKENS、SAFETY、PAUSE、CANCELLED 等映射到带样式的提示行,由 _build_stop_reason_additional_segments(:344)在显示时拼进去。
# 示意,非源码:停止原因 → 屏幕提示
if stop_reason == LlmStopReason.MAX_TOKENS:
show("\n\nMaximum output tokens reached - generation stopped.") # 红色斜体
elif stop_reason == LlmStopReason.CANCELLED:
show("\n\nGeneration cancelled by user.") # 黄色斜体
取消与回滚。 这一层还记录「上一轮是否被用户取消」和回滚状态(record_last_turn_cancellation,:192;last_turn_history_state,:188),供工具循环在中断后把历史恢复到干净点。注意:此时 LlmAgent 本身还不会调工具——它的 run_tools 继承自 ① 的空实现(agents/llm_decorator.py:1436,原样返回请求)。工具能力是下一层的事。
注意
agent_type:① 的LlmDecorator.agent_type返回AgentType.LLM(llm_decorator.py:365),②LlmAgent没重写,所以到这里类型仍是LLM。
3.3 ③ ToolAgent —— 让 agent 真的会用工具
它要解决的小问题: 模型说「我要调 search(query=...)」之后,得有人真的去执行这个函数、把结果塞回对话、必要时并行跑多个。这一层就是「手脚」。
关键设计一:混入工具循环协议。 ToolAgent 的定义是 class ToolAgent(LlmAgent, _ToolLoopAgent)(agents/tool_agent.py:50),多继承了 _ToolLoopAgent(agents/tool_runner.py:42,一个 Protocol/mixin)。于是它的 generate_impl(:368)不再是「生成+显示」,而是构造一个 ToolRunner 并 until_done()——一次调用里反复「让模型说话 → 执行工具 → 把结果喂回」直到模型不再要工具。ToolRunner 的内部循环见 03 章。
关键设计二:本地函数工具的注册与执行。 传进来的普通 Python 函数或 FunctionTool 会被 add_tool(:121)登记进 _execution_tools,并生成对应的 JSON schema 给模型看。真正执行时 call_tool(:855)按名字找到函数、run 它、把原生结果转成 MCP 的 CallToolResult。
关键设计三:并行 vs 串行。 run_tools(agents/tool_agent.py:741)先把这一批 tool_calls 规划成 PlannedToolCall,再按 should_parallelize_tool_calls 决定走 _run_parallel_tool_calls(用 gather_with_cancel 并发,:671)还是 _run_sequential_tool_calls(:714)。
# 示意,非源码:一批工具调用,够多就并发
planned = plan_tool_calls(tool_calls, ...) # 规划:哪些工具、参数、关联 id
if should_parallelize(len(planned)):
results = await gather_with_cancel(run(c) for c in planned) # 并行
else:
results = [await run(c) for c in planned] # 串行
关键设计四:把「另一个 agent」当成一个工具(agent-as-tool)。 add_agent_tool(agents/tool_agent.py:319)把一个子 agent 包成名为 agent__<name> 的函数工具。它被调用时,用 3.1 的 spawn_detached_instance 克隆出子 agent 的独立实例再跑,跑完合并用量、关闭克隆(_shutdown_agent_tool_clone,:308)。这是 fast-agent 编排(见 06 章工作流)的基本积木——父 agent 用一句话委派给子 agent,子 agent 有自己隔离 的历史和 LLM 栈。
小坑标注:
ToolAgent构造器里的tools参数指的是可执行的本地函数工具,和AgentConfig.tools(那是 McpAgent 用的 MCP 过滤表)是两回事,源码注释专门点了这一点(tool_agent.py:56)。
3.4 ④ McpAgent —— 接上外部世界(MCP + 运行时 + skills)
它要解决的小问题: 本地函数工具还不够,真正的编码 agent 要能连 MCP 服务器(拿远程工具/资源/提示词)、要能跑 shell、读写文件、按 skills 干活,还要把系统提示词里的占位符(服务器说明、skills 列表)渲染出来。
关键设计一:它就是「普通 agent」本体,并聚合了一个 MCP 聚合器。 class McpAgent(ABC, ToolAgent)(agents/mcp_agent.py:178)。注意一个容易看走眼的点:它虽然把 ABC 写进基类,却没有任何 @abstractmethod,所以可以被直接实例化——事实上工厂对普通 agent 走的正是这条路:direct_factory.py:711 直接 McpAgent(config=..., context=...) 造出实例(UI 模式下换成子类 McpAgentWithUI)。换句话说,「AgentType.BASIC 的普通 agent = 一个 McpAgent 实例」,并不需要更下游的 SmartAgent。构造时它建一个 MCPAggregator(:206)统一管理多个 MCP 服务器连接;initialize 走异步上下文 __aenter__(:406)连服务器,shutdown 关连接(:427)。运行时还能 attach_mcp_server / detach_mcp_server(:510 / :535)热插拔——这是 3.5 里 /mcp connect 的底层。聚合器内部见 05 章。
关键设计二:list_tools 变成「多源合并」。 ③ 的 list_tools 只列本地函数工具;④ 重写它(agents/mcp_agent.py:2238)先取过滤后的 MCP 工具,再按去重合并本地函数工具、terminal(shell)工具、文件系统工具、skill 读取工具、human-input 工具(_additional_runtime_tools,:2269)。于是模型看到的工具清单是「远程 + 本地 + 运行时」的并集。相应地,④ 也重写 run_tools(:1493)和 call_tool(:1150),让工具调用能路由到聚合器或本地运行时。
list_tools() 合并顺序(④ McpAgent):
MCP 聚合器工具(按 config.tools 过滤)
+ 本地函数工具(来自 ③)
+ shell / 外部终端工具
+ 文件系统工具(read_text_file / write / apply_patch …)
+ skill 读取工具
+ human-input 工具
→ 同名去重 → 交给模型
关键设计三:运行时(runtime)插槽。 shell 执行、本地文件系统、外部(ACP)运行时都是可插拔的槽位,由 _activate_shell_runtime(:909)、_maybe_enable_local_filesystem_runtime(:806)按配置/--shell/是否配置了 skills 来激活(_configure_initial_shell_runtime,:313)。配置了 skills 就会自动开 shell,因为 skills 常需要执行脚本(_ensure_shell_runtime_for_skills,:700)。
关键设计四:指令模板渲染。 系统提示词里可以写 {{agentSkills}} 之类占位符,_apply_instruction_templates(:614)在服务器连上后,把服务器说明、skills 清单渲染进最终 instruction;若配了 skills 却没放占位符还会告警。
agent_type:④ 重写为AgentType.BASIC(mcp_agent.py:2307)——所以「基础但功能完整」的 fast-agent agent 类型名叫 BASIC,而它的实现类就是可直接实例化的McpAgent。
3.5 ⑤ SmartAgent —— 交互式编码 agent 体验
它要解决的小问题: 到 ④ 已经能连一切了,但终端里的人还想要「像用 CLI 一样」的体 验:敲 /tools 看工具、/mcp connect ... 现场接一个服务器、/session 管会话、让 agent 用 smart 工具去跑一张 AgentCard 定义的子 agent。
关键设计一:注册一组「smart 工具」。 SmartAgent 构造时调 _enable_smart_tooling(agents/smart_agent.py:1870、:1701),把三个方法包成模型可调用的工具:smart(跑/校验 AgentCard 子 agent)、slash_command(执行 /...)、get_resource(读资源)。于是模型自己也能触发斜杠命令和子 agent。
关键设计二:/slash 命令路由。 slash_command 最终进 _run_slash_command_call(:971),按命令名分发到 /help、/commands、/skills、/cards、/model、/mcp、/session、/tools、/prompts 等一大票 handler。其中 /mcp connect <target> 走 _run_mcp_slash_command_call(:907),底层正是 ④ 提供的 attach_mcp_server——运行时热接一个 MCP 服务器。
关键设计三:smart 工具 = 用 AgentCard 现起子 agent。 _run_smart_call(:1402)从卡片文件加载子 agent、组成一个临时 AgentApp、按需 mcp_connect、发消息、跑完全部关闭。这让一个 SmartAgent 能把任务委派给磁盘上定义好的子 agent,而不必在代码里写死。
关键设计四:两个成品与 UI 变体。 除了 SmartAgent,同文件还有 SmartAgentsAsToolsAgent(把「agents-as-tools」编排也加上 smart 工具,:1981)和 SmartAgentWithUI(:2095,class SmartAgentWithUI(McpUIMixin, SmartAgent),再混入 MCP-UI 支持)。agent_type 在这层重写为 AgentType.SMART(:1882)。
一句话:⑤ 不改「怎么调模型/怎么调工具」,它加的是人和模型驱动这台机器的操作面板。
4. 巧妙之处(可借鉴的技术)
- 「一个可重写的
generate_impl空位」贯穿全链。 门面(send/generate)只做归一化和 tracing,真正行为落在generate_impl;每层只重写这一个方法就能层层加厚,互不打架。见llm_decorator.py:576→llm_agent.py:813→tool_agent.py:368。 - LLM 后挂 + 记住挂载参数 = 免费的换模型与克隆。
attach_llm把工厂和 kwargs 存进_llm_attach_kwargs(llm_decorator.py:436),set_model(:327)和spawn_detached_instance(:448)都靠它重建,无需重写构造逻辑。 - agent-as-tool 用「克隆独立实例」而非复用自己,天然获得状态隔离与可并发(
tool_agent.py:334的spawn_detached_instance),这是编排层能安全并行子 agent 的前提。 - 发给模型前按模型能力剔内容(
_sanitize_messages_for_llm,llm_decorator.py:927):不支持 vision/document 的模型不会被一张图直接搞崩,被删内容还留档到 error 通道。 - 能力按需自动开:配了 skills 自动开 shell(
mcp_agent.py:700),减少用户手动配置负担。
5. 边界与局限
- 这是一条单继承主链(每层一个直接父类,配少量 mixin/Protocol 如
_ToolLoopAgent、StreamingAgentMixin、McpUIMixin)。要在中间插入新能力,通常意味着改父类或新增子类,而非水平组合。 McpAgent虽把ABC写进基类,却没有任何抽象方法,因此能被直接实例化——它不是「不可实例化的抽象壳」。「普通 agent」(AgentType.BASIC)就是工厂直接new出来的McpAgent(direct_factory.py:711);想要一个「能连 MCP 但没有 smart 命令」的 agent,用它本身即可,不必到SmartAgent那一层。- provider-managed MCP 有限制:
_on_llm_attached(mcp_agent.py:867)会在 provider 不是 Anthropic/Responses 时直接抛AgentConfigError。 - 工具循环的中断/回滚/重连历史细节(如
reconcile_interrupted_history)在本章只点到接口,真正逻辑在 ToolRunner,见 03 章。
6. 代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| ① 门面基类 | src/fast_agent/agents/llm_decorator.py | LlmDecorator |
| 门面三级入口 | src/fast_agent/agents/llm_decorator.py | __call__ / send / generate |
| 可重写空位 | src/fast_agent/agents/llm_decorator.py | generate_impl |
| 挂载 LLM / 换模型 | src/fast_agent/agents/llm_decorator.py | attach_llm / set_model |
| 克隆独立实例 | src/fast_agent/agents/llm_decorator.py | spawn_detached_instance |
| 送模型前净化内容 | src/fast_agent/agents/llm_decorator.py | _sanitize_messages_for_llm |
| 流式监听转发 mixin | src/fast_agent/agents/llm_decorator.py | StreamingAgentMixin |
| ② 显示/流式/停止原因 | src/fast_agent/agents/llm_agent.py | LlmAgent |
| 停止原因文案表 | src/fast_agent/agents/llm_agent.py | STOP_REASON_ADDITIONAL_MESSAGES |
| 取消回滚记录 | src/fast_agent/agents/llm_agent.py | record_last_turn_cancellation |
| ③ 工具 agent | src/fast_agent/agents/tool_agent.py | ToolAgent |
| 工具循环入口 | src/fast_agent/agents/tool_agent.py | generate_impl(构造 ToolRunner) |
| 注册/执行本地工具 | src/fast_agent/agents/tool_agent.py | add_tool / call_tool |
| 并行/串行执行 | src/fast_agent/agents/tool_agent.py | run_tools / _run_parallel_tool_calls |
| agent 当工具 | src/fast_agent/agents/tool_agent.py | add_agent_tool |
| 工具循环 mixin | src/fast_agent/agents/tool_runner.py | _ToolLoopAgent / ToolRunner |
| ④ MCP agent(普通 BASIC agent,可直接实例化) | src/fast_agent/agents/mcp_agent.py | McpAgent |
| 多源工具合并 | src/fast_agent/agents/mcp_agent.py | list_tools / _additional_runtime_tools |
| 热插拔 MCP 服务器 | src/fast_agent/agents/mcp_agent.py | attach_mcp_server / detach_mcp_server |
| shell/文件系统运行时 | src/fast_agent/agents/mcp_agent.py | _activate_shell_runtime / _maybe_enable_local_filesystem_runtime |
| 指令模板渲染 | src/fast_agent/agents/mcp_agent.py | _apply_instruction_templates |
| 工厂直接实例化 McpAgent | src/fast_agent/core/direct_factory.py | _create_agent_with_ui_if_needed(McpAgent(...)) |
| ⑤ 智能编码 agent | src/fast_agent/agents/smart_agent.py | SmartAgent / SmartAgentWithUI |
| 注册 smart 工具 | src/fast_agent/agents/smart_agent.py | _enable_smart_tooling |
| /slash 命令路由 | src/fast_agent/agents/smart_agent.py | _run_slash_command_call |
| 运行时 /mcp connect | src/fast_agent/agents/smart_agent.py | _run_mcp_slash_command_call |
| agent_type 枚举 | src/fast_agent/agents/agent_types.py | AgentType(LLM/BASIC/SMART) |