这篇文章讨论一个问题:大模型是怎么走到 Agent 的。
“AI Agent”、“Claude Code” 这些词越来越常见,但很多介绍停留在概念层。本文按实现路径展开:从语言模型的基本能力出发,到可以在真实环境中执行任务的 Agent 系统。
大模型到 Agent 的关键发展
语言模型的本质
语言模型,本质上是一个函数。
输入是一段文字,输出是”下一个词的概率分布”。把这个过程反复执行,就得到连贯文本。GPT、Claude、Llama 的底层形式本质一致。
它能理解代码、能推理逻辑——但有一个根本性的局限:
它只能处理文字,碰不到真实世界。
不能读文件,不能跑测试,不能搜网络,不知道今天几号。它活在一个纯文字的沙盒里。
还有另一个问题——它是无状态的。每次 API 调用,它都不会自动保留上一次的上下文。只有把历史对话再次传入,它才会”记得”。没有外部机制,它无法持续执行任务。
语言模型 = 一个函数
f(tokens) → 下一个词的概率分布
强项:理解、推理、生成
局限:
× 碰不到真实世界
× 无状态(每次调用都是新开始)
× 无法主动持续做事
关键里程碑
要理解 Agent 怎么来的,需要沿着时间线看几个关键节点。
2017,Transformer。注意力机制让模型能关注序列中任意位置的信息。今天所有大模型都建立在这个基础上。
2020,GPT-3 与 In-Context Learning。不用微调,只要在 prompt 里给几个例子,模型就能学新任务。这打开了”用自然语言控制模型行为”的大门。
2022,Chain-of-Thought。Google 发现,让模型一步一步思考而不是直接给答案,推理准确率会明显提升。这让研究者意识到:推理过程本身可以被引导。
时间线(上半段)
2017 Transformer
└─ 注意力机制,所有大模型的基础
2020 GPT-3 + In-Context Learning
└─ 自然语言控制模型行为
prompt engineering 的起点
2022 Chain-of-Thought
└─ 引导推理过程
"让模型先想,再答"
ReAct:推理与行动的结合
2022 年,ReAct 被提出。
论文叫 ReAct: Synergizing Reasoning and Acting in Language Models。
核心思想是让模型在推理和行动之间交替进行。每一步推理后都可以调用外部工具,再把结果作为新的观察继续推理。
在 ReAct 之前,模型主要停留在文本生成。ReAct 之后,模型可以通过工具调用参与任务执行。
这个框架直接影响了今天所有 Agent 的设计。
ReAct 模式(2022)
Thought: 我需要知道今天的天气
Action: search("北京今天天气")
Observation: 晴,22°C
Thought: 用户问要不要带伞,不用带
Action: finish("今天晴天,不需要带伞")
推理(Reasoning) + 行动(Acting)交替
→ Agent 的雏形
Tool Use 标准化:从演示到生产
ReAct 证明了推理 + 行动可行,但工程上还有一个问题。
当时工具调用是非结构化的——模型用普通文字输出 Action: search(...),再用字符串解析来执行。一旦模型输出格式略有偏差,就全崩了。这很脆弱。
2023 年,OpenAI 推出 Function Calling,Anthropic 推出 Tool Use。工程化改进主要在这里:
模型不再用文字描述操作,而是输出结构化的调用块。调用方精确解析、执行工具、返回结果。
有了这个机制,可靠的 Agent 循环才成为可能。Agent 也因此从实验演示逐步走向可落地系统。
模型本身也针对 Tool Use 做了专门训练——它知道什么时候该调工具,怎么构造合法参数,工具返回后怎么继续推理。
2023 Function Calling / Tool Use
ReAct 时代(脆弱):
模型输出普通文字 → 字符串解析 → 容易崩
Tool Use 时代(可靠):
模型输出结构化调用块 ↓
{
"type": "tool_use",
"name": "search",
"id": "tool_abc123",
"input": { "query": "北京今天天气" }
}
→ 精确解析 → 执行 → 返回结果
推理模型与长上下文
2024 年之后,两件事让 Agent 的可用性继续提升。
一是推理模型——o1、o3、Claude 3.7 这类,把”思考过程”内化到模型本身,在生成答案前进行更深的内部推理。
二是长上下文——窗口从 4K 扩展到 200K token,Agent 能处理更长任务,减少频繁压缩历史的次数。
2024+ 推理模型 + 长上下文
推理模型(o1 / o3 / Claude 3.7)
└─ "思考"内化,不只是 prompt 引导
长上下文
└─ 4K → 200K token
更长任务,更少压缩
─────────────────────────────────────
完整时间线
2017 Transformer 基础架构
2020 GPT-3 + ICL 自然语言控制
2022 Chain-of-Thought 引导推理
2022 ReAct 推理+行动(雏形)
2023 Tool Use 结构化调用(可靠化)
2024+ 推理模型/长上下文 能力大幅提升
从模型到 Agent 需要什么
从实现角度看,语言模型要成为 Agent,需要同时具备以下能力:
推理——CoT、ReAct 给了它; 行动——Tool Use 标准化给了它; 循环——Agent 框架给了它,把”感知→思考→行动”自动循环,直到任务完成。
下面进入实现细节,先看这个”循环”如何落地。
LM → Agent 的关键能力
① 推理(Reasoning)
CoT + ReAct 给的
② 行动(Action)
Tool Use 标准化给的
结构化调用,可靠执行
③ 循环(Loop)
Agent 框架给的
感知 → 思考 → 行动 → 感知 → …
直到任务完成
Agent 是怎么搭起来的
最小的 Agent
先看代码。最小的 Agent 可以写成下面这样。
核心是一个 while True 循环。每轮三步:把用户输入发给模型,检查 stop_reason;如果模型要调工具,就执行并回填结果;如果模型输出完成,就退出。
不到 30 行,就可以构成一个完整 Agent。
关键点有三个:messages[] 承载全部状态,每次调用都带完整历史;stop_reason 是决策点;while True 负责持续推进任务。
没有这个循环,开发者就得手动充当循环体:每次工具调用后都要把结果拼回上下文,再次请求模型。
def agent_loop(query):
messages = [{"role": "user", "content": query}]
while True: # ← 循环
response = client.messages.create(
model=MODEL, system=SYSTEM,
messages=messages, tools=TOOLS,
)
messages.append({
"role": "assistant",
"content": response.content,
})
if response.stop_reason != "tool_use":
return # ← 退出条件
results = []
for block in response.content:
if block.type == "tool_use":
output = run_bash(block.input["command"])
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
messages.append({"role": "user", "content": results})
工具分发
工具分发机制并不复杂。
Tool Use API 返回结构化调用块,Agent 做一次字典查找,执行对应函数,再把结果返回给模型。
关键点是:加新工具只需要加一个 handler 和一份 schema 定义,循环本身一行不改。
Claude Code 有 20 多个工具,OpenClaw 也类似,但底层的 dispatch 逻辑完全一样。
# 工具分发表
TOOL_HANDLERS = {
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"]),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"],
kw["old"], kw["new"]),
}
# 分发逻辑(循环内)
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input)
# 加新工具 = 加一行 handler + 一份 schema
# 循环本身一行不改
会话持久化
模型是无状态的,但 Agent 需要跨会话记忆。常见做法如下。
用 JSONL 文件——每条消息追加一行 JSON 记录。每次会话开始,把文件重放一遍,重建 messages[]。对模型来说,就好像对话从没中断过。
上下文窗口是有限的,对话越长越接近上限。因此通常需要三阶段溢出恢复:先正常调用;溢出后截断工具结果;仍溢出就让 LLM 压缩旧消息为摘要;再溢出才报错。
会话持久化:JSONL 文件
每行一条 JSON 记录,追加写入:
{"type": "user", "content": "帮我重构这个函数", "ts": ...}
{"type": "assistant", "content": [...], "ts": ...}
{"type": "tool_result", "content": "Done.", "ts": ...}
重放 → 重建 messages[] → 模型感知不到中断
─────────────────────────────────────
上下文溢出:三阶段恢复
① 正常调用
↓ 溢出
② 截断工具结果(保留摘要)
↓ 还溢出
③ LLM 生成历史摘要,替换旧消息
↓ 还溢出
报错
系统提示词的分层组装
在实际系统里,系统提示词通常不是硬编码字符串,而是每轮对话前动态组装。
层级越靠前,对模型行为的影响越强。例如把 SOUL.md 放在第 2 层,可以稳定控制输出风格。
一个常见设计是 Skills 两层注入:系统提示词只放技能名称,成本更低;模型需要时再调用 load_skill 工具加载完整内容,按需消耗 token。不把所有领域知识都放进系统提示词,有助于控制上下文成本。
系统提示词 8 层动态组装(每轮重建)
Layer 1 Identity "你是谁"
Layer 2 Soul 性格、行为风格 ← 越靠前影响越强
Layer 3 Tools 工具使用指南
Layer 4 Skills 按需加载的领域知识
Layer 5 Memory 长期记忆 + 本轮召回
Layer 6 Bootstrap 启动上下文文件
Layer 7 Runtime 当前时间、模型 ID
Layer 8 Channel "你正在通过 Telegram 回复"
─────────────────────────────────────
Skills 两层注入
Layer 4(便宜):技能名称列表 ~100 tokens/个
工具调用(按需):完整内容 ~2000 tokens/个
load_skill("pdf") → 返回完整文档
记忆系统
Agent 还需要长期记忆,例如用户偏好和过往对话中的关键信息。
OpenClaw 的做法:记忆存两层,常驻文件加每日日志;每次用户发消息,用 TF-IDF 搜索相关记忆,把召回结果注入系统提示词第 5 层。
这套方案可以用纯 Python 实现,不需要向量数据库。对很多场景来说,TF-IDF 已经够用,引入向量数据库反而会增加系统复杂度。
记忆系统
存储层:
MEMORY.md 常驻记忆(用户偏好、重要事实)
YYYY-MM-DD.jsonl 每日记忆日志
召回:
用户消息 → TF-IDF 搜索 → top-k 记忆片段
→ 注入 Layer 5
特点:
✓ 纯 Python,无外部依赖
✓ 不需要向量数据库
✓ 够用就好,不过度工程
主流 Agent 的实现
Claude Code 和 OpenClaw
下面对比两个常见实现。
Claude Code 是 Anthropic 的命令行编程 Agent。核心就是前面说的 while True 循环加工具分发,工具集有 20 多个,覆盖文件读写、bash 执行、搜索、网页抓取。一个亮点是 str_replace 编辑模式——不让模型输出整个文件,而是输出”把哪段替换成哪段”,精确可靠。系统提示词里定义了详细的行为约束:先读再写、验证结果、路径沙箱保护。
OpenClaw 是一个更通用的 Agent 网关框架,可以接 Telegram、飞书、CLI 等多个通道,支持多 Agent 路由和后台定时任务。核心机制完全一样,只是外面包了更多层:持久化、路由、可靠性重试、并发调度。
两者的核心都可以概括为:while True 循环 + 工具分发表,差异主要在外层工程系统。
Claude Code
用途:命令行编程 Agent
工具:~20 个(文件/bash/搜索/网页)
亮点:str_replace 编辑,路径沙箱
本质:while True + 工具分发
OpenClaw
用途:多通道 Agent 网关
通道:Telegram / 飞书 / CLI
亮点:多 Agent 路由,后台心跳/Cron
本质:while True + 工具分发
+ 持久化 / 路由 / 重试 / 并发
─────────────────────────────────────
两者共同的内核
while True:
response = LLM(messages, tools)
if stop_reason != "tool_use": break
results = dispatch(response)
messages.append(results)