最近好几个读者私信问我同一个问题:Agent 到底是怎么"学习"的?为什么同样一个模型,有的人调出来的 Agent 像个聪明助手,有的人调出来就像个复读机?说实话,这个问题问到了点子上,因为我在做 Agent 项目的过程中越来越确认一个判断——绝大多数"模型不够聪明"的抱怨,本质上是上下文没编好,和模型本身关系不大。
这篇是 Agent 精读系列的第二篇。上一篇我拆了 Agent 的整体运行框架,把"计划—执行—观察"这条主链路讲清楚了。这一篇聚焦两个关键概念:模型的学习,与上下文的构成。这两者之间的关系,打个比方就是:学习决定了模型"本来会什么",上下文决定了模型"这次能发挥出什么水平"。咱们要把 Agent 做好,两个都得搞清楚,而且上下文这块,恰恰是大多数人最容易忽略、也是最容易拿到回报的地方。
1. 先厘清一个前提:Agent 里所谓的"学习"到底指什么
1.1 三种"学习"的边界:预训练、微调与上下文学习
我第一次带团队做 Agent 项目时,产品经理问了我一个灵魂问题:"我们这个机器人能不能越用越聪明?"我当时愣了一下,因为这个问题背后藏着一个特别常见的认知混淆——在普通用户眼里,越用越聪明意味着模型本身在进化;但在工程实践里,模型本身几乎不进化,进化的是它每次接收到的上下文。
严格来说,大模型有三种"学习"方式,它们的成本、生效时间、作用范围完全不同,但市面上大多数讨论把这三者搅成了一锅粥。
**预训练(Pre-training)**是最底层的学习。模型在海量语料上学习语言的统计规律,这个过程需要几千张显卡跑几个月,消耗的资源难以想象。作为开发者,我们不碰这个,也没必要碰。预训练决定了模型的"底子",底子好的人理解能力强,底子差的你再怎么包装也是白搭。这就好比一个员工的基本功,是在学校里打下的,入职之后你再怎么培训,也不可能把基础能力完全重塑。
**微调(Fine-tuning)**是在预训练基础上,用特定数据继续训练,调整模型权重。理论上讲,微调可以让模型学会某种风格、某种领域知识,但在实际项目里,这条路有一堆现实障碍:一是需要准备高质量的训练数据,清洗、标注、格式化一套流程下来,小团队很难撑住;二是训练本身有成本,即使是最便宜的 LoRA 方案,也得有 GPU 资源和时间投入;三是微调效果很难预测,你可能训练了三轮,发现模型开始复读训练数据里的固定句式,反而把通用能力丢了。
第三种,上下文学习(In-context Learning),这才是我要重点讲的。它不改变模型权重,而是通过改变输入给模型的内容——也就是"上下文"——来引导模型输出。你给模型看两个例子,它就能模仿着给出第三个;你告诉它"只输出 JSON 格式",它就真的不再说废话。这个"学习"发生在推理阶段,成本为零,效果立竿见影,而且随时可调、完全可控。
这里有个关键认知:对 Agent 开发者来说,99% 的情况下我们做的"学习"都是上下文学习。理解这一点,你才能真正理解为什么"上下文构成"这件事如此重要——它不是锦上添花,而是 Agent 智能的核心载体。你在用户面前展示的所谓"智能",其实是"模型底子"和"上下文质量"的乘积,上下文这一项,你完全可以把控。
1.2 为什么大多数人不需要做微调
我见过不少团队,上来就提需求——"我们要微调一个专属模型",理由是"通用模型不懂我们业务"。但实际上,他们遇到的那些所谓"不懂业务",绝大多数通过更好的提示词、更好的上下文结构、或者检索增强(RAG)就能解决。
什么时候才真正需要微调?我这几年的经验是三个场景。第一是输出格式硬约束,比如你要求模型必须输出严格嵌套的 JSON Schema,而且字段嵌套很深,提示词怎么写都容易格式漂移,这时候微调确实有效。第二是专业术语与领域黑话,比如医疗、法律、金融领域有大量非标准表达,通用模型没见过,检索也检索不到,微调能帮模型"内化"这些表达。第三是固定风格的批量生产,比如你要用模型批量产出风格高度统一的内容,微调可以帮你降低提示词的复杂度。
除此之外,我强烈建议你把精力放在上下文工程上,原因特别直白:微调是"改模型",改完就很难回退,而且业务一变就得重新训;上下文工程是"改输入",随时可以调,成本低、副作用小、迭代快。在 Agent 这种高频变化的场景里,快速试错的价值远大于一次性的模型改造。打个比方,微调像是给员工报了个长期培训班,上下文工程像是每次开会前给员工发一份清晰的会议资料——后者一天能改三次,前者半年才能改一次,你说哪个更适合敏捷迭代?
2. 上下文的本质:一段精心编排的信息流
2.1 上下文不是聊天记录,而是"证据链"
第二个常见的认知混淆,是把上下文简单理解为"聊天记录"。聊天记录只是上下文的一部分,而且往往不是最重要的部分。
一个标准的 Agent 请求,它的上下文通常由五类内容构成。第一类是系统指令(System Prompt),这是 Agent 的"宪法",定义角色、目标、语气、约束和工作流程。系统指令放在上下文的最前面,决定模型以什么身份、按什么规则来处理后面的一切。我见过太多人把系统指令写成一句"你是一个助手",这等于给 Agent 配了一部只有一行字的宪法,后面全靠临场发挥,效果自然飘忽不定。
第二类是对话历史(Memory),即用户和 Agent 之前的交互记录。对话历史的长度和质量,直接影响模型对当前问题的理解。但注意,对话历史不是越长越好,尤其要警惕早期的错误回答被原样保留——模型看到自己以前答错过,就很可能被带偏,沿着错误思路继续滑。
第三类是工具结果(Tool Results)。Agent 调用搜索引擎、数据库、计算器之后返回的结果,对模型来说是"新证据",权威性最高,但噪声也最大。工具返回一堆无关信息,模型照样会被干扰。第四类是检索资料(RAG Context),从知识库、文档库检索出来的相关片段,用来补充模型的"知识盲区",但检索质量参差不齐,片段之间可能互相矛盾。第五类是用户当前输入(User Query)。
我把这五类内容叫做"上下文的信息流"。Agent 的智能水平,很大程度上取决于这条信息流编排得是否清晰、是否聚焦。你给模型喂什么"证据",它就用什么"证据"来推理——这和法官判案看证据链是一个道理。你在上下文里放了五条有效证据,模型就能给出准确判断;你在上下文里塞了五十条互相矛盾的碎片,再好的模型也只能瞎猜。
2.2 Token 预算:上下文不是越大越好
现在很多模型都在卷上下文窗口,1M token 全量可用的话题,相信大家都刷到过。先解释一下 1M token 是什么意思——Token 是模型处理文本的最小单位,粗略可以理解为"半个到四分之三个汉字"。1M token 意味着模型一次能读大约 70 万汉字的内容,相当于好几本书的量。理论上是天大的好消息,但实际用起来,这里藏着两个隐蔽的问题。
一个是注意力衰减。Transformer 架构中,模型对所有输入 token 一视同仁地"关注",但当输入特别长时,模型对中间段落的注意力会明显下降。也就是说,你把一本 300 页的书全部塞进上下文,模型真正"看进去"的,可能只有开头和结尾的部分。我做过一个粗略测试:在同一任务上,把支持 128K 上下文窗口的模型分别喂 2K、8K、32K 的上下文,32K 那一组的回答质量反而低于 8K 组,原因就是中段信息没有被有效利用。
另一个是信息密度稀释。上下文越长,噪声越多,模型在推理时需要压制的干扰信号就越多。一个 500 token 的高密度上下文,在大多数任务上比 5000 token 的稀薄上下文效果更好。这就像开作战会议,你把会议室里塞满了无关紧要的报表和背景资料,指挥官反而抓不住重点。
再加上成本与延迟——虽然长上下文的定价已经在降,但 1M token 的推理耗时仍然是几十 token 的几十倍,在交互场景里让用户等几十秒才收到回复,体验基本归零。所以我的建议是:上下文窗口大,是给你"装得下"的底气,而不是让你"往死里塞"的借口。真正要做好上下文工程,你要做的恰恰是在有限预算内做减法。这里给一个我在实践中验证过的参考预算:单次请求总 token 若按 8K 来规划,系统提示词控制在 1K 以内,对话历史 3K 左右,工具结果和检索资料合计 3K,用户输入 1K。你可以按自己的场景调比例,但记住原则——系统提示词别贪多,历史记录常做摘要,工具结果要清洗。
3. 实操:把上下文工程落到自己的 Agent 里
3.1 先给角色分好工:消息层级的正确用法
现在主流的 Agent 框架,不管是什么开源方案,基本都支持结构化消息,消息有明确的角色字段:system、user、assistant、tool。很多人在实际开发中,根本没有充分利用这个结构,什么内容都往 system 或者 user 里塞,最后上下文变成一团乱麻。我总结了一套角色分工的规矩,供你参考。
system 里只放稳定不变的规则。角色设定、目标说明、输出约束、工作流程,凡是会随对话进度变化的内容,都不应该放在 system 里。如果系统提示词超过 2000 token,你就要警惕了——大概率是你把不属于"宪法"级别的内容也塞进去了,该挪出去就挪出去。
user 消息记录用户的真实输入。每次用户说了新的一句话,就新建一条 user 消息,不要和之前的 user 消息合并。合并会导致模型无法区分"旧需求"和"新需求",在长对话里,这个区分一旦丢失,模型就会把用户现在的要求和历史需求混着处理,输出结果自然四不像。
assistant 消息记录模型自己的输出。这一条很关键:模型上一轮的输出要不要放进下一轮的上下文?我的经验是必须放,但要选择性放。放是为了让模型"记住自己说过什么",避免前后矛盾;选择性放,是因为有些中间过程的思考内容、工具调用参数等,对最终回答没有意义,放进上下文反而是负担。现在很多框架把模型的"思考过程"和"输出结果"分开返回,其实就是给这个需求留的口子。
tool 消息记录工具的原始返回。工具返回的内容要单独成组,而且最好附上工具名、调用参数、耗时这些元信息。这不仅是给模型看的,更是给调试工具看的——你排查问题的时候,能不能快速定位到"是哪次工具调用的返回把模型带偏了",全靠这些元信息。
3.2 三种上下文管理策略:截断、摘要与检索
聊完了消息结构,再来说实际管理上下文长度的三个策略。这三个策略不是三选一,而是要根据场景混用,分别应对不同形态的信息。
**截断(Truncation)**是最简单粗暴的方案:上下文超长了,就把最旧的对话历史砍掉,只保留最近 N 轮。这个方法的问题是,早期关键信息会丢,比如用户在 20 轮之前做过一个明确偏好声明,你在第 21 轮就把它遗忘了。我一般只在两种场景用纯截断:一是对话本来就短、旧信息不再需要的场景,比如一次性问答;二是作为兜底策略,在一切其他手段都失败时,保证请求能发出去。
**摘要(Summarization)**是我最推荐的主流方案:对话历史超过阈值时,先让模型对早期对话做一次压缩,把摘要留在上下文里,原始对话丢出去。这相当于给 Agent 做了一份工作日志,重要的结论保留,琐碎的细节清除。注意,摘要不是只做一次就完事,要形成链路:第一轮摘要之后,后续对话继续累积,再超阈值就做第二轮摘要。而且,每轮摘要要放在一个独立的消息位里,让模型清楚这是"历史摘要",不是"当前对话",避免模型把摘要内容误认为刚发生的事。
**检索(Retrieval)**适用于超长文档场景:不把整篇文档放进上下文,而是先做切片、建索引,每次根据用户问题检索出最相关的片段放进去。这和 RAG 的思路一致,适合"知识问答型 Agent"。检索的难点在于切片粒度——切小了,上下文太碎,模型抓不住意思;切大了,可能混入无关信息。我一般按语义段落切片,每片控制在 200 到 400 token 之间。
| 策略 | 适用场景 | 优点 | 缺点 | 额外成本 |
|---|---|---|---|---|
| 纯截断 | 短对话、一次性问答 | 实现简单,零额外调用 | 丢失早期关键信息 | 无 |
| 摘要 | 长对话、多轮任务 | 保留关键信息,上下文体积可控 | 多一次模型调用,增加延迟 | 中 |
| 检索 | 知识问答、超长文档 | 上下文小而准,可扩展 | 检索质量不稳定,需维护索引 | 中高 |
在我的真实项目里,典型做法是:用摘要管对话历史,用检索管外部知识,用截断做最后兜底。三管齐下,基本能覆盖 90% 的上下文管理需求。
3.3 一个可复用的上下文模板
下面给一个精简但完整的上下文模板,你可以直接拿去改造。我用 Python 风格伪代码写,因为不同框架的消息结构略有差异,但大同小异:
messages = [] # 1. System message:稳定规则 messages.append({ "role": "system", "content": """ 你是XX业务的智能助手。你的职责是帮助用户完成XX任务。 工作流程: 1. 先确认用户需求,必要时提问澄清。 2. 需要实时信息时,调用工具获取。 3. 最终输出要简洁、结构化,包含结论和依据。 约束: - 不要编造数据,没有依据的内容必须说明"未核实"。 - 涉及不确定的结果时,给出置信度并说明原因。 """ }) # 2. 历史摘要(如果有) if summary: messages.append({ "role": "system", "content": "以下是此前的对话摘要,请基于摘要理解上下文:" + summary }) # 3. 对话历史(最近N轮) for turn in recent_turns: messages.append({"role": "user", "content": turn.user_input}) messages.append({"role": "assistant", "content": turn.assistant_output}) # 4. 工具结果(针对本轮需要) for tool_result in tool_results: messages.append({ "role": "tool", "name": tool_result.name, "content": tool_result.content }) # 5. 用户当前输入 messages.append({"role": "user", "content": current_user_query})有几个细节你可能会踩坑,我提前标出来。
摘要放在 system 角色里,是为了让模型把它当作背景信息,而不是对话内容,避免模型把摘要里的东西当成"刚刚说过的话"。有些框架不支持多条 system 消息,那就把摘要放在 user 角色,但记得加一个明显的前缀标记,比如"[历史摘要]",让模型一眼分清。
工具结果的放置位置,我推荐紧跟在触发调用工具的那条 assistant 消息之后,而不是把所有工具结果集中在用户输入之前。这样链条更清晰:模型先看到"我决定调用工具",再看到"工具返回了结果",最后看到"用户的问题",整个推理链路是连贯的。如果所有工具结果堆在一起,模型很难判断每个结果到底是为哪一步服务的。
当前用户输入永远放最后一位。这是新手最容易犯的错误,也是我最想强调的一点。上下文的位置信息对模型注意力影响很大,排在末尾的内容权重最高。把用户当前输入放在最后,模型会优先理解"现在要干什么",而不是被前面的历史信息带跑。
4. 上下文工程常见的坑与排查
4.1 一张问题速查表
做上下文工程的时间越长,越会发现大多数 Agent 的翻车现场,不是模型笨,而是上下文出了问题。我把最常见的几个现象、原因和解决方案整理成了一张速查表:
| 现象 | 可能原因 | 排查方向 | 解决建议 |
|---|---|---|---|
| Agent 忘记早期设定 | 上下文被截断,早期 system 或用户偏好被丢弃 | 检查 messages 最后一轮截断点 | 用摘要策略替代纯截断 |
| 答非所问 | 用户当前输入被夹在历史消息中间,未放在最后 | 检查消息排列顺序 | 强制把当前 query 作为最后一条 user 消息 |
| 引用过时信息 | 历史中保留了旧结论,模型被带偏 | 检查 assistant 历史中是否有过时回答 | 更新信息时,在历史中标记"该信息已失效" |
| 编造工具返回数据 | 工具结果未正确传入,模型只能靠"猜" | 检查 tool 消息是否在上下文中 | 确保工具结果完整传入,增加"没有数据必须声明"的约束 |
| 长上下文后效果骤降 | 上下文过长,注意力衰减 | 统计单次请求 token 数 | 做摘要或检索,控制上下文在 5K token 以内 |
| 输出格式漂移 | system 中的格式约束不明确或位置太靠后 | 检查 system 提示词是否被后续信息淹没 | 把格式示例放在 system 中,并在用户输入后追加格式提醒 |
这张表我在团队内部一直贴在墙上,每次 Agent 表现异常,先对着表自查一遍,大概率能省下半天排查时间。
4.2 排查上下文问题的三个手段
排查上下文问题,光看现象是不够的,得能"看到"模型实际收到的上下文长什么样。我的排查流程基本分三步。
第一步,看原始请求日志。所有成熟的 Agent 框架都应该有 debug 模式,把每次请求的完整消息结构和 token 统计打出来。重点看三点:消息顺序对不对、每类内容占了多大比例、有没有意料之外的重复内容。我遇到过一个很诡异的问题——模型总是重复用户最后一句话。查日志才发现,框架在构造上下文时,把用户输入同时塞进了 system 和 user,模型被搞懵了,分不清自己该回答什么。
第二步,做最小化实验。把出问题的场景抽出来,只保留最少量的上下文消息,看模型是否还会犯错。如果删掉某段历史后,模型就恢复正常了,说明问题就出在那段历史里。然后再用二分法——保留前半段、保留后半段分别测试,就能定位到具体哪一句内容污染了模型。
第三步,对照不同模型的差异。同一个上下文,换个模型效果可能完全不一样。你在模型 A 上调试得恰到好处的提示词模板,用到模型 B 上可能就崩了。这并不说明模型 B 更差,而是不同模型的上下文敏感度和指令遵从能力有差异。我的经验是,框架层上下文模板尽量写得"平实",少用花哨修辞和复杂嵌套结构,让所有模型都能稳定理解;如果要追求极致效果,那就锁定一个模型,针对它的脾气精调模板。
4.3 一次上下文污染排查实录
去年我维护的一个 Agent 项目,遇到一个很诡异的问题:某个用户连续追问三次之后,Agent 开始答非所问,并且把"抱歉,我之前的回答可能不准确"当成了固定开头语。我第一反应是模型抽风了,但换一个模型复现,问题依旧,这就基本排除了模型自身的问题,而是上下文出了状况。
打开 debug 日志后,真相大白。第三次用户追问时,上下文里保留了前几轮的全部内容,其中第一轮恰好因为工具超时返回了一个错误结果,模型当时的回答里包含了"我得出的结论可能不准确"这句话。后面几轮,模型为了"修正自己",反复引用这句不准确的话,整个推理被带进了死循环。
这就是典型的历史污染:早期错误回答没有被处理,被当成"待修正的事实"一路带下去了。解决办法分两步走。第一,在消息构造时,对工具结果做质量判断——超时或错误返回不要直接塞给模型,而是替换成一条明确标注"工具调用失败,结果为占位值"的消息,让模型知道这是异常而不是事实。第二,在系统提示词里加一条规则——"如果对历史中的某个结论不确定,可以明确要求用户重新确认,而不是基于错误历史继续推理"。两条都加上之后,这个问题从 30% 的复现率降到了 2% 以下。
再分享一个框架层面的细节。很多 Agent 框架内置了"自动记忆"功能,会把历史对话压缩成一个"记忆"对象。这功能好用,但你要注意记忆更新的时机。我踩过的坑是:框架在每次用户输入前就先更新记忆,而记忆更新的提示词里包含了用户输入,导致模型对用户输入产生了"重复关注",输出时优先响应记忆更新请求,而不是用户的实际问题。解决办法是在框架配置里修改更新时机,让记忆更新放到一次完整对话结束之后。但要注意"结束"的判定条件,别让记忆永远不更新,那就矫枉过正了。
5. 上下文工程的进阶思路与技术底座
5.1 上下文工程的本质是信息流治理
如果你把上下文工程当成"写提示词的技巧",那它的天花板很低;但如果你把上下文工程理解成"Agent 系统的数据流设计",那空间就完全打开了。
我之前在做一个订单查询 Agent 时,一开始按常规思路设计:用户提问,检索订单数据,拼接上下文,生成回答。效果总是差一口气。用户问"我上个月总共花了多少钱",Agent 响应很慢,因为检索模块把过去一年的订单都捞出来了,上下文塞得满满当当,模型处理起来又慢又容易漏重点。
后来我换了个思路。在调用模型之前,先让一个"查询解析模块"——可以用更便宜的模型,也可以用规则引擎——把自然语言问题转成结构化的查询参数:时间范围、用户 ID、统计口径。然后拿着结构化参数去数据库查询,只把聚合结果放进上下文。这样一来,上下文里不再是一堆原始订单记录,而是一行"2025 年 5 月总支出为 3,216.50 元,较上月增长 12%"的明确结论。模型只需要基于结论做解释,回答又快又准。
这个例子说明,上下文工程的本质是信息流的治理:你要规划哪些信息进入"上游",如何加工,以及以什么形态呈现给模型。这个"查询解析、数据聚合、上下文呈现"的链条,就是一套典型的上下文数据流设计。想通了这一点,你就会发现市面上讨论的 RAG、Agent 记忆、工具调用标准化,本质上都是在做同一件事:把原始信息加工成模型能高效利用的上下文形态。
5.2 给想深入的人:三条进阶路径
这篇内容偏实操,但如果你想把上下文工程真正做深,继续往上走,我给你三条路径参考。
第一条,学透 Transformer 的关键原理。别以为做应用层就不用懂模型内部机制。上下文工程要解决的注意力衰减、位置编码影响、token 切分差异,底层全都能在 Transformer 原理中找到解释。不用自己去训练模型,但至少要知道为什么 token 顺序重要、为什么不同模型对上下文的敏感度不同。先把注意力机制和位置编码这两块吃透,很多玄学问题就变成了数学问题。
第二条,读一遍主流 Agent 框架的源码。现在开源的 Agent 框架很多,不要只看 README,要读它的上下文构造代码,看它在拼接消息时做了哪些处理、暴露了哪些记忆管理接口。你会惊讶地发现,很多你自己踩过的坑,框架作者早就给出了默认解法,只是你可能没开那个开关。读源码是最快的抄作业方式,比看一百篇社区教程都管用。
第三条,把提示词和上下文模板纳入版本管理。把提示词当作代码来管:每个模板有版本号、变更记录、回滚能力。模型输出不稳定,很可能是你在某个版本加了一句不起眼的话,破坏了整体平衡。版本化管理能让你在回归测试时快速定位到那个"元凶"改动。我现在所有 Agent 项目的提示词模板都放进 Git 仓库,和代码一样走分支、评审、发布流程,这个习惯帮我避免了很多次"明明没改代码,效果却变了"的灵异事件。
结尾
聊到最后,分享一个我在实际项目里反复验证过的体会:高质量的上下文工程,收益往往大于换一个更强模型。很多团队遇到 Agent 效果不好,第一反应是"换更大的模型",换完之后发现还是老样子——因为他们把同样糟糕的上下文喂给了一个更聪明的模型,聪明模型好一点,但成本贵了十倍。反过来,把上下文结构做扎实、信息流理顺,同一个模型的效果能提升一个档次,而且零额外成本。
按照我个人经验,下次你的 Agent 表现不佳,先别急着怪模型,打开日志看看上下文里到底有什么。那个让模型"犯傻"的信号,往往就藏在某一段你以为无关紧要的历史里。把一个 Agent 的上下文当成真正的产品来设计、测试、迭代,你会发现它的智能水平,比你想象中更依赖你写的每一段输入。这也是我在 Agent 精读系列里最想传达的一件事。