ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

上下文工程:让AI真正懂你

上下文工程:让AI真正懂你 在前面的文章中我们已经理解了 Agent 中的 Message、Tool 以及长、短期记忆。这些内容分别用于解决不同的问题当 Agent 真正运行起来时它们最终都会汇到同一个地方模型的上下文。模型这一轮应该看到哪些 Message哪些 Tool Result 还需要保留长期记忆要不要读取知识库检索出的内容应该放多少运行时信息又该以什么方式加入这些问题共同构成了上下文工程Context Engineering要解决的事情根据当前任务从不同的信息来源中选择、整理和补充内容为每一次模型调用准备合适的上下文。一、什么是上下文对于大模型来说上下文Context可以简单理解为模型在生成当前这次回答之前能够看到的全部信息。例如用户发送帮我分析一下这个接口为什么返回 401。如果模型只看到这一句话它几乎无法判断问题出在哪里。但如果同时看到实现该接口的代码 项目的认证配置 请求 Header 错误日志 之前的相关对话它才有足够的信息进行分析。这些内容共同构成了模型当前的上下文。在普通 LLM 调用中上下文通常比较简单可能只有System Prompt 用户输入而在 Agent 中情况就复杂一些。一次模型调用看到的内容可能包括System Prompt 历史 Message 当前用户请求 Tool 定义 Tool Result 短期记忆 长期记忆 RAG 检索结果 运行时信息而且这些内容会随着 Agent 的运行不断变化。所以上下文并不是某一段 Prompt也不是单指聊天记录。只要某段信息最终进入了当前这次模型调用它就是模型上下文的一部分。理解了这一点我们再来看什么是上下文工程。二、上下文工程是什么以前做大模型应用时经常会讨论 Prompt Engineering。它主要解决的问题是Prompt 应该怎么写模型才能更准确地理解任务。例如补充角色、目标、约束条件、Few-shot 示例以及输出格式。对于简单的模型调用这往往已经够用了。但 Agent 不一样。Agent 会持续对话、调用工具、读取记忆、检索知识库还可能根据运行状态动态改变自己的行为。所以真正需要考虑的问题已经从Prompt 应该怎么写变成了这一轮调用模型时 到底应该给它哪些信息这就是上下文工程。可以把它理解成根据当前任务从各种信息来源中挑选、整理和补充内容组成这一轮模型真正需要的上下文。对比 Prompt Engineering 和 Context Engineering它们的区别如下表维度Prompt EngineeringContext Engineering主要对象Prompt整个模型输入处理内容指令、示例、格式Prompt、Message、Tool、Memory、RAG 等是否动态通常比较固定经常随运行过程变化主要目标把任务说清楚把当前需要的信息准备好需要说明的是Prompt Engineering 仍然是重要的。只是到了 Agent 中它只是上下文工程的一部分。三、上下文从哪里来要管理上下文先要知道 Agent 每一次调用模型时信息可能从哪里来。在前面的多篇文章中我们多次提到 Agent 的运行过程调用模型 ↓ 判断是否调用 Tool ↓ 执行 Tool ↓ 返回 Tool Result ↓ 再次调用模型这个过程会重复进行直到模型生成最终结果。在这期间模型每一次拿到的上下文都可能不同。System PromptSystem Prompt 用来说明 Agent 的基本职责和行为要求。例如你是一个企业知识库助手。 回答公司制度问题时优先查询内部知识库。 资料不足时不要自行补充事实。这类内容通常比较稳定。但在实际项目中System Prompt 也可能动态变化。例如管理员和普通员工看到的操作权限不同那么 Agent 可以根据当前用户身份在模型调用前临时加入不同的说明。所以 System Prompt 不一定要在应用启动时全部写死。当然如没有特殊目的不建议动态调整 System Prompt。MessageMessage 是当前 Thread 中已经发生过的对话和执行记录。常见的消息包括HumanMessage AIMessage ToolMessage例如用户查询订单 10001 AI我帮你查一下 Tool订单状态为已发货 AI订单已经发货这些消息会成为后续模型调用的重要上下文。对话持续得越久Message History 通常也会越来越长。Tool ResultAgent 调用工具后工具返回的数据通常也会进入后续模型调用。这里需要注意工具返回的数据会极大影响上下文的长度。例如搜索工具返回了十几 KB 的网页内容数据库工具一次查回几百条记录这些内容都可能继续留在上下文里。所以很多 Agent 后期 Token 增长很快并不只是因为聊天记录太长还有一个常见原因Tool Result 太多、太大。Runtime Context运行时上下文Runtime Context保存的是程序本次运行需要使用的信息例如user_id 用户角色 租户 ID 权限 数据库连接 API Client 环境配置这类信息不一定需要直接给模型看。例如数据库连接对象只是 Tool 执行数据库查询时需要模型根本不需要知道它的具体内容。所以 Runtime Context 更像程序运行时的依赖。需要时Middleware 或 Tool 可以读取它再决定哪些信息需要转换成模型能够使用的上下文。State 与 StoreState 保存当前 Thread 的状态也是短期记忆的重要载体。例如当前 Message 当前任务状态 已经执行过哪些步骤 中间结果Store 更适合保存跨 Thread 的长期信息例如用户偏好 历史事实 长期配置 用户画像数据放在哪里是一回事。这一轮要不要给模型看是另一回事。上下文工程的核心就是决定上述这些数据中哪些应该在当前任务中提供给模型。四、为什么不能什么都给理论上来说我们刚才提到的那些数据都有用因此很容易形成一种思路既然这些信息以后可能有用那就都给模型。刚开始这样做确实很省事。但系统稍微复杂以后问题就会越来越明显。最直接的是 Context Window 有长度限制。Message、检索文档和 Tool Result 一直增加最终可能超过模型能够接受的最大输入长度。但即使没有达到这个上限上下文过多也不一定有帮助。例如用户问帮我看看这个接口为什么返回 401。模型真正需要的可能只有接口代码 认证方式 请求 Header 最近的错误日志 相关配置如果同时再给它三天前讨论过的数据库设计 完整项目 README 几十个 Tool 定义 完整用户画像 以前的所有搜索结果这些东西虽然都属于同一个项目但对当前问题帮助不大。它们反而会增加输入 Token 响应时间 无关信息干扰 工具选择难度所以上下文工程不是简单地解决“窗口不够大”。它要解决的是模型这一轮判断时到底需要知道多少。即使模型支持很大的 Context Window也没有必要把所有能找到的信息都塞进去。窗口更大只代表可以放更多东西并不代表应该放更多东西。五、上下文怎么控制实际开发中上下文管理常见的做法主要有选择、裁剪、摘要和动态注入。选择最理想的方式不是先把所有内容放进去再删除而是一开始就只取当前需要的信息。例如一个企业 Agent 有 40 个 Tool。如果用户现在只是查询订单那么这一轮可能只需要订单查询 物流查询 退款查询财务报表、代码仓库、服务器运维等 Tool没有必要一起暴露给模型。Tool 本身也会占用上下文。而且 Tool 越多模型需要做的选择越复杂选错工具的可能性也会增加。Memory 和 RAG 也是同样的道理不是读取全部记忆 而是读取相关记忆 不是加载整个知识库 而是检索相关文档 不是保留全部历史 而是保留当前任务需要的历史裁剪对于 Message History最简单的方法是直接裁剪。例如只保留最近 20 条 Message或者只保留最近 8000 Token 的历史这种方式实现简单也不会多产生一次模型调用。问题也很明显被删掉的信息当前这一轮就看不到了。所以它更适合那些主要依赖近期对话的场景。摘要如果旧消息不能直接删除就可以把较早的内容压缩成摘要。例如前 40 条 Message ↓ 一段会话摘要 最近 10 条原始 MessageLangChain 提供了SummarizationMiddleware来处理这类长对话。from langchain.agents import create_agent from langchain.agents.middleware import SummarizationMiddleware agent create_agent( modelgpt-5.5, tools[], middleware[ SummarizationMiddleware( modelgpt-5.4-mini, trigger(tokens, 4000), keep(messages, 20), ) ], )运行结果历史消息超过设定阈值后 较早消息 → 压缩成摘要 最近消息 → 保留原文 后续调用 → 使用“摘要 最近消息”这里要区分两种处理方式。如果只是模型调用前临时删除一些 Message影响的只是这一次调用。State 中原来的消息仍然可以保留。而SummarizationMiddleware会更新 State 中的消息历史所以后续模型调用看到的也是压缩后的结果。虽然都能减少上下文但两者对后续运行的影响并不一样。六、上下文可以按需生成真实系统里还有很多信息并不适合长期写在 System Prompt 中。例如当前用户角色 用户回答偏好 当前上传的文件 任务执行阶段 业务权限 当前环境这些信息每一次运行都可能不同。更合适的方式是在模型调用之前根据当前状态临时生成。LangChain 可以通过 Middleware 或dynamic_prompt读取 State、Store 和 Runtime Context再构造这一轮需要的 System Prompt。假设 Store 中保存了用户的回答偏好而 Runtime Context 中提供当前user_id。from dataclasses import dataclass from langchain.agents import create_agent from langchain.agents.middleware import dynamic_prompt, ModelRequest from langgraph.store.memory import InMemoryStore dataclass class UserContext: user_id: str store InMemoryStore() store.put( (preferences,), user-001, { communication_style: concise, }, ) dynamic_prompt def build_system_prompt(request: ModelRequest) - str: user_id request.runtime.context.user_id preference request.runtime.store.get( (preferences,), user_id, ) prompt You are a technical assistant. if preference: style preference.value.get( communication_style, balanced, ) if style concise: prompt \nKeep answers concise and avoid unnecessary background. return prompt agent create_agent( modelgpt-5.5, tools[], middleware[build_system_prompt], context_schemaUserContext, storestore, ) result agent.invoke( { messages: [ { role: user, content: 解释一下 HTTP 401 和 403 的区别。, } ] }, contextUserContext(user_iduser-001), ) print(result[messages][-1].content)运行结果大致如下401 表示身份认证失败或尚未完成认证 例如 Token 缺失、无效或已经过期。 403 表示服务器已经识别用户身份 但当前用户没有访问这个资源的权限。这里没有把整个用户画像放进模型。Middleware 先通过user_id找到用户偏好再把回答尽量简洁这一条真正有用的信息加入当前 System Prompt。如果下一个用户偏好详细解释那么下一次调用生成的 Prompt 又可以不同。这样一来上下文不再是一份长期不变的模板而是根据当前任务临时组装。七、Memory 和 RAG 怎么参与从上下文工程的角度看它们其实都是信息来源只是解决的问题不同。短期记忆关心的是这个 Thread 前面发生过什么例如用户刚才说了什么 Agent 调用了哪些工具 任务已经做到哪一步LangChain 中这些内容通常保存在 State并可以通过 Checkpointer 持久化。长期记忆关心的是换一个 Thread 以后还应该记住什么例如用户喜欢简洁回答 用户所在部门 以前确认过的偏好 长期保存的用户信息这类数据可以保存在 Store 中需要时再读取。RAG 解决的是回答当前问题需要查哪些外部资料例如公司制度 产品文档 技术手册 数据库记录 网页内容于是一个 Agent 完全可以同时使用三种来源State ↓ 当前会话的信息 Store ↓ 跨会话保存的信息 Retriever ↓ 当前问题需要的外部资料 ↓ 筛选和整理 ↓ LLM重要的原则依然是不能因为这些信息都有用就全部取出来。实际开发中更常见的做法是按需分批获取。例如只取最近几轮聊天记录而不是全部历史用户记忆按活跃度或相关性筛选RAG 文档限制在 Top 5工具结果仅保留最近一次调用。也可以通过设置上下文窗口预算为不同类型信息分配额度超出的部分丢弃或压缩。这样做能避免上下文失控。八、Middleware 怎么管理理解到这里再看 Middleware 就比较容易了。create_agent构建在 LangGraph 之上Middleware 可以插入 Agent 的运行过程在模型调用和 Tool 调用前后处理数据。模型调用之前可以做修改 System Prompt 筛选 Message 读取长期记忆 选择 Tool 选择 Model 设置 Response FormatTool 执行之后也可以做整理 Tool Result 更新 State 写入 Store 提取需要长期保存的信息对话越来越长时还可以摘要旧消息 清理历史 Tool Result 限制上下文大小因此一个稍完整的 Agent 可以形成这样的结构State / Store / Runtime Context / Retriever ↓ Middleware ↓ Prompt / Message / Tools ↓ LLMMiddleware 更适合处理的是“模型调用前后怎么整理信息”而不是负责保存所有信息。例如 Store 中有几十条用户记忆。Middleware 可以先找到与当前问题有关的两三条再放进 Prompt。一个 Agent 有几十个 Tool也可以根据当前任务只把真正需要的工具交给模型。如果这些逻辑散落在每一个业务调用中很快就会变得难以维护。放到 Middleware 中统一处理会清晰很多。九、多 Agent 中的上下文当一个 Agent 要做的事情越来越多上下文往往也会越来越复杂。例如一个研究任务可能需要搜索网页 读取十几个页面 分析内容 比较多个来源 整理结论如果这些步骤全部发生在主 Agent 中那么搜索结果、网页正文、工具调用记录都会不断进入主 Agent 的 Message History。等真正开始写答案时上下文中已经堆满了大量中间过程。这时 Multi-Agent 有一个很实用的价值把不同任务的上下文分开。例如Main Agent │ ├── Research Agent │ ├── Search │ ├── Read │ ├── Analyze │ └── Compare │ ←── 研究结果Research Agent 可以在自己的上下文里完成搜索和分析。主 Agent 不需要看到几十次 Tool 调用只需要拿到最后整理好的研究结果。LangChain 的 Subagents 模式就是这种思路。不过上下文隔离不等于什么都不传。真正需要考虑的是主 Agent 给 Subagent 什么 Subagent 自己保留什么 最后返回给主 Agent 什么如果主 Agent 把完整 Message History 原样传给所有 Subagent隔离的意义就小了。反过来如果只给一句帮我研究这个问题Subagent 又可能缺少完成任务所需的背景。所以 Multi-Agent 设计中一个很重要的问题就是上下文应该在哪里断开又应该在哪里传递。十、实际开发中的选择上下文工程没有固定模板。不同 Agent 需要的信息完全不同但有一些做法比较实用。第一先从简单上下文开始。不要一上来就同时加入长期记忆 自动摘要 动态 Prompt 十几个 Middleware Multi-Agent 复杂 RAG先把最基本的 Agent 跑通。模型缺什么再补什么上下文真的变长了再做裁剪和摘要。第二区分“系统里有什么”和“模型看到什么”。数据库可以保存十万条历史记录但当前模型调用可能只需要三条。Store 负责保存。Retriever 或业务代码负责查找。Middleware 再决定怎样交给模型。第三Tool Result 尽量只保留后续推理真正需要的数据。例如数据库查出了 500 行记录但模型只需要总数 最近 5 条 几个统计值那就应该在 Tool 内先整理而不是直接返回完整 JSON。第四长期信息尽量按需加载。姓名、语言、固定回答偏好这类经常使用的信息可以动态加入 Prompt。历史订单、以前讨论的问题、大量用户事实则更适合需要时再查。第五不要只看 Token。实际运行中还应该一起观察输入 Token 响应时间 Tool 调用次数 任务成功率 工具误选情况 回答准确性把上下文压得太短也可能导致模型缺少必要信息。总结上下文工程不是独立于 Agent 的模块而是设计 Agent 输入信息的方式。对模型而言上下文就是这一轮调用中它能看到的全部内容包括 System Prompt、Message、Tool Result、短期与长期记忆、RAG 检索结果及部分运行时信息。核心问题不是“让模型看到更多”而是“这一轮它应该看到什么”。State 记录当前会话Store 保存跨会话信息Retriever 查找外部知识Middleware 在模型调用前进行筛选、裁剪、摘要和注入。多 Agent 同理拆开任务可避免中间信息堆满 Context Window。开发时可先检查每次调用带了哪些 Message、Tool Result 返回了多少内容。上下文工程是让 Agent 在需要时刚好拿到所需信息。
返回列表