目标:通过 6 个循序渐进的步骤,从零实现一个可用的 AI Agent,并真正理解它背后的工作机制。
为什么要自己构建 Agent?
这两年,大家已经很熟悉各种 AI 编程助手了。
Claude Code、Qwen Code、GitHub Copilot……它们能读代码、写代码、解释复杂逻辑,甚至还能自己调用工具,一步一步把任务做完。第一次用的时候,很多人都会有同一种感觉:
它已经不像一个"聊天机器人",更像一个真的会工作的开发者。
不能只停留在会用的阶段,还要清楚它如何能做到这些。本文将通过从零构建一个属于你自己的 Agent,帮助你深入理解以下核心概念:
Agent如何管理上下文
为什么能调用工具并据此继续推理
MCP 是如何把外部能力接进来的
Skills 为什么能降低提示词成本
Subagent 又是如何解决上下文污染问题的
这篇文章不讲复杂框架,也不讲大而全的系统设计,而是从零开始,搭一个最小但完整的 Agent。
本文将通过 6 个渐进步骤,带你构建一个类似 Claude Code 的 AI Agent。当你走完这 6 步后,你不仅会更会用 Agent,也会真正理解 Agent。
步骤 1:记住对话
Commit:
be88f03- 实现带有流式输出的基础对话
作用
如果没有对话历史,模型每一轮都只能看到当前输入。那它既记不住你刚才说过什么,也不知道自己上一轮回答了什么,更不可能形成连续任务执行能力。
在这一步中,我们会完成:
用 CLI(命令行)接收用户输入
接入 LLM(通过 OpenAI 协议)
支持流式输出,让生成结果边产出边显示
管理完整对话历史
注意:这一步构建的只是一个带记忆能力的聊天机器人,而不是 Agent。但这一层非常关键,因为后面所有能力,无论是工具调用、任务分解,还是子智能体协作都要建立在"历史上下文可持续传递"这个基础上。
核心组件
1. 会话管理器 它负责把消息按顺序存下来,例如:
System → User → Assistant → User → Assistant ...
一轮请求模型时,把这整段历史一起发给模型,这样模型看到的就不再是一次性的输入,而是一段连续对话。
2. LLM 客户端 封装 OpenAI API,并支持流式返回。 通过 AsyncGenerator,可以在 Token 到达时立即输出,从而获得更自然的交互体验。
3. CLI 循环 整体逻辑非常简单:
读取用户输入 → 调用模型 → 输出结果 → 继续等待下一轮输入
这个持续运行的 while (true)循环维持了整个交互式会话
步骤 2:工具调用
Commit:
dae56bc- 包含 6 个工具的函数调用
作用
语言模型本身只能生成文本,它可以告诉你"应该执行 ls",但它并不能真的去执行 ls。
而一旦支持函数调用,模型不再只是"建议你怎么做",而是可以直接触发系统能力,当前代码实现已经内置了常用的工具函数,例如:
- 执行 Shell 命令(
bash) - 读写文件(
read/write/edit) - 搜索代码(
glob/grep)
模型第一次拥有了感知和操控外部世界的能力,此时聊天机器人变为 Agent。
核心逻辑:ReAct 循环
几乎所有 Agent 的底层都可以抽象为一个简单循环:
推理 → 行动 → 观察 → 再推理
也就是经典的 ReAct 模式(Reasoning + Acting),基本过程如下:
- 推理 (Reasoning):模型判断下一步该做什么
- 执行 (Acting) :调用工具执行动作
- 观察 (Observing) :读取工具结果,并把结果作为新的上下文继续推理
伪代码逻辑:
function agent_loop(user_message):
conversation.add(user_message)
while true:
# 推理:LLM 决定做什么
response = llm.generate(conversation, available_tools)
# 检查 LLM 是否要使用工具
if response.has_tool_calls():
for tool_call in response.tool_calls:
# 行动:执行工具
result = execute_tool(tool_call)
# 观察:将结果加入对话历史
conversation.add(tool_result(result))
# 继续循环 - LLM 将根据新的观察结果进行推理
else:
# 没有工具调用 = 任务完成
return response.text这个循环看起来很简单,但它已经足够支撑一个基础 Agent 的运行机制。它的威力来自一个闭环:
推理 → 行动 → 观察 → 再推理
一旦工具结果持续回流给模型,模型就不再是凭空猜答案,而是在不断根据真实反馈修正自己的行动。
这也是为什么 Agent 看起来比普通对话更"像在解决问题",因为它真的在一步一步试、一步一步看、一步一步往前推进。
步骤 3:MCP——接入更多外部服务
Commit:
d238b9d- 支持模型上下文协议 (Model Context Protocol)
作用
当你给 Agent 几个内置工具后,它已经能做不少事,但很快你就会碰到一个边界:
内置工具再多,也始终是你自己预先写死的那一小部分能力。
现实中的任务往往需要更多外部系统支持,比如:
- 操作 GitHub
- 查询数据库
- 做网络搜索
- 收发邮件
- 接入社区已有工具
核心组件
MCP 客户端:本地 MCP Server 通常通过 stdio 连接,远程 MCP Server 则可以通过 HTTP Stream 或 SSE 访问。
自动加载机制:启动时读取 mcp-servers.json,自动连接所有已配置的 MCP Server,并拉取它们暴露的工具定义。
统一工具注册表:把 MCP 返回的工具格式转换成和内置工具一致的结构,统一注册到同一个工具列表里。
对于模型来说,它最终看到的是一组扁平化工具列表,模型不需要知道工具来自哪里,只需要知道:
- 工具名称
- 参数结构
- 能完成什么事
步骤 4:TODO 管理——对抗遗忘
Commit:
6e25680- 任务追踪
作用
当你给 Agent 一个多步骤任务时,比如:
"重构 auth 模块,补充测试,并更新文档。"
理论上它应该按顺序推进。但实际上,很多 Agent 做着做着就会出现这些问题:
- 忘记原始目标
- 只完成一部分任务
- 中途被局部信息带偏
- 在多个子任务之间来回跳转
这种现象称为上下文遗忘(Context Fade)。模型想出一个计划,但这个计划如果只存在于它当下的上下文里,就很容易在几轮工具调用之后被新的信息淹没掉。
TODO管理功能
TODO列表**把模型脑海中的隐式计划,变成外部持久、可更新的显式状态。**不再依赖模型去记住计划,而是让模型随时能看到计划,它的主要作用为:
固定任务锚点:复杂任务不再只存在于短期上下文中,而是被写成明确列表。
形成执行轨迹:任务状态可以从 pending 更新为 in_progress,再更新为 completed,让执行过程具备连续性。
约束执行:当任务以列表方式可视化后,模型更容易保持按步骤推进,而不是被局部问题带偏。
所以 TODO 管理不只是一个"用户体验功能",更是一种真正提升 Agent 执行稳定性的机制。
核心组件
TODO工具:它本质上仍然是一个工具。你可以提供一个像 todo_write 这样的内置工具,并在系统提示里添加如下描述:
IMPORTANT: Use the todo_write tool to track your tasks when working on multi-step tasks.
- Create a todo list at the start of complex tasks
- Update task status as you work (pending -> in_progress -> completed)
- This helps the user see your progressAgent触发TODO示例
User: "重构 auth.ts 以提取验证器"
Agent 调用 todo_write:
任务列表:
⏳ 读取 auth.ts
⏸ 提取验证函数
⏸ 创建 validators.ts
⏸ 更新导入引用
[Agent 工作中...]
✓ 已完成: 读取 auth.ts
⏳ 提取验证函数
...步骤 5:SubAgent
Commit:
0f25524- 专用子智能体
功能
从系统设计角度看,SubAgent 是一种面向复杂任务的分治架构。主 Agent 负责面向用户理解目标、拆解任务和编排流程,而 SubAgent 负责完成某个明确范围内的子任务,例如信息检索、数据分析、代码执行或内容生成。
SubAgent的作用不只是分担工作,更重要的是把复杂问题拆成多个边界清晰的执行单元。这样既能降低单个 Agent 的上下文负担,提高结果稳定性,也便于系统进行并行处理、模块优化和错误定位。所以,在复杂工作流中SubAgent是实现可扩展性与工程化落地的关键机制。
实现
实现上同样添加内置工具delegate_task,主Agent根据系统提示词调用delegate_task工具,比如:
delegate_task(agent_name="explorer", task="Find all API endpoints")
当主 Agent 调用这个工具时,你就启动一个独立的子智能体去执行任务。它有自己的系统提示词、自己的历史记录、自己的工具权限。
例如已经内置的SubAgent:
explorer负责搜索代码库结构researcher负责读代码、理解逻辑planner负责产出实施计划
SubAgent 的核心价值并不只是分工,而是让主 Agent 的上下文始终保持干净和聚焦。
步骤 6:技能 (Skills)——知识可以按需加载
Commit:
aa15e81- 按需加载知识技能
作用
Agent 并不天然具备所有领域知识,你当然可以把各种规范、流程、最佳实践全部塞进系统提示词里,但这种做法很快会遇到三个问题:
成本高:每轮请求都要重复携带大量 Token
噪音大:不相关知识会分散模型注意力
系统提示词脆弱:每次增删内容,都可能影响整体行为
Skills 的作用就是把这些"知识性提示"从主系统提示词中拆出来,变成一种按需注入的外部知识单元。
Tools 和 Skills 的区别
这个区分非常重要:
Tools:能做什么
- 例如
bash、read、write
- 例如
Skills:怎么做
- 例如代码审查规范、PDF 处理流程、MCP 开发指南
Skills 的工作方式
Skills 采用"元数据常驻、内容按需加载"的方式:
- 启动时扫描
skills/目录 - 只加载每个 Skill 的简短元数据
- 在系统提示词中仅列出可用 Skill 名称
- 当模型判断需要某个 Skill 时,调用
load_skill - 将完整
SKILL.md内容作为工具结果注入上下文 - 模型基于这份知识完成当前任务
这样做的好处很明显:
- 平时几乎不占上下文
- 真正需要时再加载
- 技能系统可以独立扩展,不必频繁改动基础系统提示词
SKILL.md 格式
---
name: code-review
description: Comprehensive code review with security checks
---
# Code Review Skill
You are now an expert code reviewer. Follow this checklist:
## Security
- [ ] SQL injection vulnerabilities
- [ ] XSS vulnerabilities
...
## Performance
- [ ] N+1 query problems
...
[Detailed instructions, examples, output format...]6步回顾
到这里你已经完成了一个简单的Agent
| 步骤 | 能力 | 关键技术 | Commit |
|---|---|---|---|
| 1. 对话基础 | 记忆 | 历史管理 + 流式传输 | be88f03 |
| 2. 工具调用 | 行动 + ReAct | 函数调用 + 工具循环 | dae56bc |
| 3. MCP | 生态系统 | 协议适配器 + 统一注册表 | d238b9d |
| 4. TODO | 规划+透明度 | 状态追踪 | 6e25680 |
| 5. 子智能体 | 专业化 | 任务委派 + 上下文隔离 | 0f25524 |
| 6. 技能 | 领域专长 | 元数据索引 + 按需加载 | aa15e81 |
如果用一张结构图来概括,它大概是这样的:
[步骤 1] 对话历史维持上下文
↓
[步骤 2] 工具注册表 + ReAct 循环 = Agent 核心
↓
[步骤 3] MCP 扩展可用工具库
↓
[步骤 4-6] 添加内置工具,增强决策能力:
├─ 更新 TODO (进度可视化)
├─ 委派给子智能体 (分工)
└─ 加载技能 (注入领域知识)
↓
所有这些都反馈进 ReAct 循环:
推理 (Reasoning) → 行动 (Acting) → 观察 (Observing) → (重复直到完成)这 6 步并不是彼此割裂的功能堆叠。它们共同组成了一个不断增强的 Agent 架构:
- 对话历史提供基础记忆
- 工具调用提供行动能力
- MCP 提供生态扩展
- TODO 提供任务锚点
- SubAgent 提供上下文隔离
- Skills 提供按需知识注入
每一个模块都是在不破坏前一层能力的基础上,持续增强 Agent 的完成复杂任务能力。
完整代码
代码仓库:mini-agent
be88f03: 步骤 1 - 基础对话功能dae56bc: 步骤 2 - 工具调用 + ReAct 模式d238b9d: 步骤 3 - 支持 MCP (模型上下文协议)6e25680: 步骤 4 - TODO 任务管理0f25524: 步骤 5 - 子智能体 (Subagents)aa15e81: 步骤 6 - 技能系统 (Skill System)
你可以使用 git checkout <commit> 来查看代码的演变过程。