过去的一年里,AI Agent 已经从"概念验证"迅速走向"工程实践"。
相比传统的「Prompt + LLM」模式,Agent 具备以下关键特征:
- 🧠 自主推理(Reasoning)
- 🔧 工具调用(Tool Use)
- 🗂️ 状态 / 记忆管理(Memory)
- 🔁 多轮决策循环(ReAct Loop)
- 🤝 多 Agent 协作(Multi-Agent,进阶)
市面上已经出现了不少成熟框架,例如 AgentScope、AutoGen、LangGraph 等。但它们往往工程复杂、抽象层级高,对于想真正理解 Agent 内部机制的开发者并不友好。
于是我写了这个项目:
nano-agentscope:一个"能跑、够用、看得懂"的 AI Agent 框架
它的目标不是功能齐全,而是帮助你从底层理解:Agent 到底是如何工作的。
项目地址:
👉 https://github.com/brianxiadong/nano-agentscope
我写 nano-agentscope 的动机很朴素:把 Agent 这套东西拆到"我能完全掌控每一环"的程度。
真实工程里,Agent 失败通常不是"代码报错"那么简单,而是下面这些更隐蔽的问题:
- 上下文断了:你以为模型看到了工具结果,但其实没喂进去。
- 协议不一致:内部消息结构和 SDK 消息结构混着用,一扩展就炸。
- 工具调用闭环没打通 :模型确实发起了
tool_call,但你回写tool_result的格式不对,下一轮推理完全忽略。 - 排障没有抓手:看不到 LLM 请求、看不到工具入参、看不到工具结果,最后只能"改 prompt 祈祷"。
所以一期我只做一个目标:
- 把单智能体的 ReAct(Reasoning + Acting)闭环跑通,并做到可 debug。
读完这一篇,你应该能做到两件事:
- 从源码层面说清楚:一个最小 Agent 框架需要哪些抽象、各自边界在哪里。
- 真出问题时,你知道从哪一环开始定位,而不是盲目调 prompt。
项目介绍
nano-agentscope 是一个极简版的 AI Agent 框架,设计目标非常明确:
- ✅ 代码量小,可完整通读
- ✅ 模块职责清晰,结构直观
- ✅ 保留主流 Agent 框架的核心思想
- ❌ 不追求"开箱即用"的复杂能力
你可以把它理解为:
AgentScope 的"教学版 / 最小实现"
当前 nano-agentscope 的核心结构如下:
text
nano_agentscope/
├── agent.py # Agent 核心执行引擎
├── message.py # 消息与内容结构定义
├── model.py # LLM 抽象与实现
├── formatter.py # Prompt / 消息格式化
├── memory.py # 对话与上下文记忆
├── tool.py # 工具注册与执行
└── tests/ # 基础测试
nano-agentscope 的 Agent 本质上是一个 ReAct(Reason + Act)循环执行器。
整体流程可以简化为:
text
用户输入
↓
消息(Message)
↓
Prompt 格式化(Formatter)
↓
大模型(LLM)
↓
解析模型输出
├─ 普通回答 → 返回给用户
└─ 工具调用 → 执行 Tool
↓
工具结果
↓
更新 Memory
↓
继续下一轮推理
一句话总结:
Agent = LLM + 状态 + 工具 + 循环控制
而 nano-agentscope 做的事情,就是把这几个要素拆开、写清楚。
1)我的拆分原则:让"变化"停留在最小范围内
写框架最容易犯的错是:把所有东西都塞进 Agent.reply(),然后靠 if/else 维护世界和平。nano-agentscope 的拆分原则是:把变化隔离在边界上。
- 模型厂商会变(OpenAI / DashScope / 自建网关)
- 工具来源会变(本地函数 / 远程 MCP / 业务 RPC)
- 消息协议会变 (不同 SDK 的
messages结构、tool calling 结构差异)
因此我把核心链路拆成 5 个可替换的模块:
Msg:内部统一消息协议(我只在内部认这一套)Memory:保存上下文(一期先最简单)Formatter:内部协议 → 某个 SDK 的请求协议Model:某个 SDK 的响应 → 内部统一响应Toolkit:工具注册 / schema 生成 / 执行 / 结果回写
ReActAgent 只做"编排",不做"适配"。
2)先定"内部协议":不要让 SDK 协议污染你的业务逻辑
我见过太多项目一开始直接用 OpenAI 的 messages 结构写业务逻辑,后面想接第二家模型就重构到吐血。
nano-agentscope 的策略是:内部统一用 Msg + ContentBlock。
Msg代表一条消息(role/name/content/metadata/...)content不仅是字符串,而是多个内容块:texttool_usetool_resultimage(简化)
一个关键的小设计是:即使 content 是字符串,也要能"自动视作文本块",这样上层逻辑永远按"块"处理。
落点代码:message.py 的 Msg.get_content_blocks()。
3)ReAct 主循环:最小闭环长什么样?
ReAct 的本质非常简单:
- Reasoning:把上下文喂给模型,让它决定要不要用工具
- Acting:如果模型要用工具,就执行工具,把结果回写上下文
- 重复直到模型不再请求工具
在 nano-agentscope 里,这个闭环几乎一眼就能看懂:
- 落点代码:
agent.py的ReActAgent.reply() - 判断是否继续循环:看输出里有没有
tool_use块
最重要的一句工程经验是:
- 工具结果不回写记忆,就等于没调用过工具。
这是绝大多数工具调用失败的根因(而不是 prompt 写得不好)。
4)Formatter:工具调用最容易"卡死"的就是这层
如果你只看模型输出,很容易误判:
- 模型已经发起
tool_calls了 - 但你回写
tool_result的消息格式不对 - 下一轮模型拿不到工具结果,于是继续瞎编/继续调用
因此 nano-agentscope 里把 Formatter 当成"协议转换器",并且默认实现 OpenAIFormatter:
- 内部
tool_use→ OpenAI 风格tool_calls - 内部
tool_result→ OpenAI 风格role="tool"+tool_call_id
落点代码:formatter.py 的 OpenAIFormatter.format()。
工程上你可以把它理解为:
- 内部协议稳定 (
ContentBlock) - 外部协议可替换(换模型 SDK 时换 Formatter 即可)
5)Model 适配:把"千奇百怪"的响应统一成 ChatResponse
上层 Agent 真正关心的只有两件事:
- 这次输出有没有
tool_use? - 如果没有,最终文本是什么?
因此 Model 层的核心职责就是:
- 把不同 SDK 的响应解析成统一的
ChatResponse(content=[...blocks]) - 把 token/metadata 统一收敛,便于日志和统计
落点代码:model.py。
这层看起来"很 boring",但它把上层 Agent 从 SDK 细节里解放出来,是框架能扩展的关键。
6)MCP:把远程工具当成本地工具用(先把心智模型立住)
一期我不展开 MCP 的协议细节,只建立一个工程上好用的心智模型:
- MCP server 提供工具列表
- 客户端把每个远程工具包装成一个"可 await 的函数对象"
Toolkit把它当成本地函数一样注册/执行
落点代码:mcp.py 的 MCPToolFunction。
为什么这套方式工程上好用?因为它让"工具来源"对 Agent 来说是透明的:
- 本地工具/远程工具都走同一套 tool calling 闭环
- 远程网络抖动可以在 MCP 这一层做超时/重试兜底(不污染 Agent 主逻辑)