从聊天到干活:LLM 是怎么一步步变成 Agent 的

从 LLM 只能聊天到能干活,中间经历了哪几层关键的工程叠加?

你有没有想过一个问题——

几年前我们用 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 年一篇论文提出的思路,核心骨架就是一个持续循环:

每一轮循环做三件事:

  1. Thought:LLM 推理当前步骤。比如「发现了这个 bug,需要先看一下这个文件的源代码」
  2. Action:LLM 输出一个工具调用。比如 read_file("src/main.py")
  3. 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 有四条铁律:

  1. 工具签名即文档——每个工具的名称、参数、返回值本身就是使用说明
  2. 结果必须可验证——每一步的输出需要有明确的验证标准
  3. 错误不可沉默——所有失败必须可见并结构化回传(Error-as-Data)
  4. 渐进性披露——不要让 Agent 同时面对所有信息,按需呈现

除此之外,权限控制、反馈回路、可观测性也都是 Harness 的重要组成部分——这些是值得单独展开的话题,这里先不深入。

总结一下就是:Agent = LLM + Harness。LLM 负责推理,Harness 负责把它装好,让它能干活。

回头看整条演化路径

把这几层串起来,就是一条清晰的演化路径:

LLM到Agent的六层演化路径

LLM → Agent 演化路径:六层叠加,每层解决上一层的新问题(点击图片查看大图)

每一层都在解决上一层暴露出来的新问题。LLM 本身没有变「更聪明」,但加上这些工程组件之后,它从一个只会聊天的文本预测器,变成了一个能干活、能调接口、能写代码、能自己决定下一步的 Agent。

从 Function Calling 到 ReAct 到 Harness,每一层都不复杂。就像搭积木——每一块都很简单,但搭对了顺序,就能搭出一个能干活的东西来。

使用 Hugo 构建
主题 StackJimmy 设计