你有没有想过一个问题——
几年前我们用 LLM,还只是拿来聊天查资料,把它当做一个更高效的搜索引擎。你问它一句,它答一句,答完就结束了。怎么现在它们突然就能干活了?调接口、读文件、写代码、自己决定下一步做什么——这些东西跟文本生成有什么关系?
答案是:从「聊天」到「干活」,中间叠加了好几层工程手段。每一层解决一个问题,又会暴露一个更深层的问题。我们这篇文章就沿着这条问题链往下分析。
第一层:LLM 本身什么也干不了
我们都知道 LLM 的本质是一个一次性的文本预测器:你给它输入一段文本,它就输出一段文本,然后结束。总体而言,它有四个根本限制:
| 限制 | 什么意思 |
|---|---|
| 无状态 | 每次调用是独立的。不记得上一次聊了什么,除非把历史都塞回去 |
| 无行动能力 | 只能输出文字。不能调 API、不能读文件、不能执行命令 |
| 知识截止 | 训练完之后就停在那个时间点了,不知道最新消息 |
| 单步推理 | 复杂任务需要多步,它只能走一步 |
这些限制在简单问答场景下不明显——你问一句它答一句。但一旦任务变成「帮我排查一下这个 bug」,它需要:读文件 → 看报错 → 查文档 → 改代码 → 跑测试 → 看结果。这根本不是一次 LLM 调用能完成的。
那么:怎么让 LLM 做一个完整的事?
答案是:不给它加任何东西,它确实什么也干不了。但你可以围绕它叠加工程层——就像给一个只有大脑的躯体装上感官、手脚和神经系统。
蓝色是 LLM 核心,绿色是工程层,整个大框就是 Agent。后面的章节就按这个顺序一层一层拆开来看。
第二层:Function Calling——让 LLM「说」出要调什么工具
在 Function Calling 出现之前,想让 LLM 调用工具,做法很原始。
你要在 prompt 里告诉 LLM:「你有哪些工具可以用、每个工具是干什么的、参数是什么」。然后 LLM 在回答的时候,把工具调用写在文本里——比如输出一段 JSON,或者一段特定格式的文字。你在代码里写一堆正则表达式去匹配这段文字,解析出工具名和参数,再去执行。
这个方法的问题很明显:LLM 输出文本是不受约束的。今天它输出 {"tool": "get_weather", "args": {"city": "北京"}},明天它可能输出 让我来查一下北京的天气吧![调用:get_weather(北京)]。格式稍微一变,你的正则就匹配不上了。而且每换一个模型,输出的格式习惯不一样,你的正则又得重写。
2023 年中,OpenAI 推出了 Function Calling。这个改动看似不大,但影响深远。
OpenAI 做的事其实就两件:第一,在模型微调阶段加入了大量「用户提问 → 模型输出 tool_calls」的训练样本,让模型学会在合适的时机输出结构化工具调用;第二,在 API 层面加了一个 tools 参数,开发者直接把工具定义传进去,模型输出里就会自动带上 tool_calls 字段。
至于微调数据具体怎么构造、工具调用的能力边界在哪里——这是个可以单独写一篇文章的话题,这里先不展开。
Function Calling 做的事很简单:模型在输出时,除了生成文本,还能输出一个结构化的对象,里面明确指定了要调什么工具、传什么参数。
// 以前的写法:模型用文字描述
输出:"让我查一下天气。\n\n```json\n{\"tool\": \"get_weather\", \"args\": {\"city\": \"北京\"}}\n```"
// Function Calling:模型直接输出结构化调用
输出:{
tool_calls: [{
name: "get_weather",
args: { city: "北京" }
}]
}
区别在哪?第一个是「模型用文字描述了自己想做什么」,第二个是模型结构化地输出函数名称和参数列表——API 层收到后直接按这个字段去路由和执行,不需要任何文本解析。
说到这里你可能会好奇:LLM 不是拿对话和文本训练出来的吗,它怎么会「知道」要输出结构化的 tool_calls?
答案在微调阶段。OpenAI 在推出 Function Calling 之前,在微调数据里加入了大量「用户提问 → 模型输出 tool_calls」的训练样本。模型在预训练时就已经见过代码和 JSON,微调只是教它在什么时机输出:
最后一个阶段是关键。更直白一点:模型不是「学会调工具」,而是学会了「在需要调工具的场景下,输出一段看起来像 tool_calls 的 token 序列」。 它以为自己只是在接着写文本,但对使用者来说,它在干活。
清楚了 Function Calling 的原理之后,一个新的问题自然浮现了:光能说一次还不够——一个完整的任务往往需要多步操作:查了数据、分析、再查、再分析。能不能让模型不止调一次,而是一直干下去直到任务完成?
这就引出了下一层:ReAct 循环。
第三层:ReAct 循环——从一次调用到持续行动
一次 Function Calling 只能完成一步。要完成一个复杂任务,需要多步。这时候就需要一个循环。
ReAct(Reasoning + Acting)是 2022 年一篇论文提出的思路,核心骨架就是一个持续循环:
每一轮循环做三件事:
- Thought:LLM 推理当前步骤。比如「发现了这个 bug,需要先看一下这个文件的源代码」
- Action:LLM 输出一个工具调用。比如
read_file("src/main.py") - Observation:工具执行结果塞回下一轮 prompt,LLM 看到了继续推理
用代码实现也就几十行:
while not done:
prompt = build_prompt(messages, observation)
response = llm.chat(prompt)
action = parse_action(response)
if action.type == "final_answer":
done = True
else:
observation = execute_tool(action.name, action.args)
messages.append(response)
messages.append(observation)
这个循环就是所有现代 Agent 的基座。Claude Code、Codex、Manus、Cline——不管上面叠了多少层,最底层跑的都是这个循环。
ReAct 循环很好用,任务越复杂跑的轮数就越多。但跑起来之后,人们很快发现一个问题:每轮都要把全部历史塞回 prompt,跑了 8 轮之后光历史记录可能就已经占了 30K token。而且很多早期的对话其实已经没用了——第一轮的推理早就被后续结果覆盖了。
Token 越积越多,效率越来越低——能不能在保留必要上下文的同时,不让循环撑爆窗口?
解决方法有几个方向:
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 滑动窗口 | 只保留最近 N 轮对话,丢弃最旧的 | 简单任务,早期对话不重要 |
| 摘要压缩 | 把旧对话用 LLM 压缩成一段摘要 | 需要长期记忆的任务 |
| 结构化上下文 | 把历史信息按类型组织,LLM 按需读取 | 复杂任务 |
| 渐进性披露 | 不一次给全部信息,先给概要再展开 | 长文档分析 |
Mitchell Hashimoto(Harness Engineering 提出者)说过一句话:长上下文不是银弹。关键是上下文的质量和组织方式,不是长度。
对话管理解决了「一次任务内」的持久化问题。但关掉会话之后呢?下次遇到同样的问题,能不能复用上次的排查经验?
这就引出了下一层:记忆系统。
第五层:记忆——让经验跨会话复用
想象一个场景:上周花了半天排查了一个连接池超时问题,找到了根因。这周又遇到一个类似的报错,但你又得从头排查一遍——因为上次的排查结果在上次对话里,已经关了,没了。
这就是记忆要解决的问题。
记忆可以分为几个层次:
| 层次 | 存储方式 | 生命周期 | 示例 |
|---|---|---|---|
| 上下文窗口 | prompt 里的消息列表 | 当前会话 | 刚才的对话历史 |
| 文件存储 | Markdown / JSON 文件 | 持久化 | MEMORY.md、USER.md |
| 向量数据库 | Embedding 检索 | 持久化 | RAG 知识库 |
| 结构化数据库 | PostgreSQL 等 | 持久化 | 业务实体、修复记录 |
关键点:分层不是替代关系,是互补关系。 文件层存结构化知识,向量层存语义可检索的内容,上下文层存当前会话的状态。实际工程中,通常会为每个 Agent 分配独立的数据库 schema,同时把技能和记忆以文件形式备份到代码仓库——既能在会话中快速检索,又能在仓库里版本回溯。
有了循环、上下文管理、记忆,LLM 终于能完整地完成一个任务了。但这些东西是一块一块拼上去的——Function Calling、ReAct、上下文管理、记忆,各是一套独立的组件。到了这一步,人们开始想:能不能把这些零散的组件装进一个统一的框架里?
第六层:Harness——给 LLM 装一个操作系统
Harness Engineering 的核心思想是:围绕 LLM 构建一个「操作系统」——包括工具系统、上下文管理、权限控制、反馈回路、可观测性。
没有 Harness 的话,做 Agent 就是 LLM + 循环 + 几个工具,拼起来就完事了。Harness 的视角不一样——它认为 Agent 能不能用好,瓶颈往往不在模型本身,而在于怎么装。
LangChain 的 Deep Agents 项目有一个实验很能说明问题:仅仅优化 Harness(不改模型),Terminal Bench 2.0 排名从第 30 位直接跃升至第 5 位。
Harness 有四条铁律:
- 工具签名即文档——每个工具的名称、参数、返回值本身就是使用说明
- 结果必须可验证——每一步的输出需要有明确的验证标准
- 错误不可沉默——所有失败必须可见并结构化回传(Error-as-Data)
- 渐进性披露——不要让 Agent 同时面对所有信息,按需呈现
除此之外,权限控制、反馈回路、可观测性也都是 Harness 的重要组成部分——这些是值得单独展开的话题,这里先不深入。
总结一下就是:Agent = LLM + Harness。LLM 负责推理,Harness 负责把它装好,让它能干活。
回头看整条演化路径
把这几层串起来,就是一条清晰的演化路径:
LLM → Agent 演化路径:六层叠加,每层解决上一层的新问题(点击图片查看大图)
每一层都在解决上一层暴露出来的新问题。LLM 本身没有变「更聪明」,但加上这些工程组件之后,它从一个只会聊天的文本预测器,变成了一个能干活、能调接口、能写代码、能自己决定下一步的 Agent。
从 Function Calling 到 ReAct 到 Harness,每一层都不复杂。就像搭积木——每一块都很简单,但搭对了顺序,就能搭出一个能干活的东西来。