上下文引擎:Agent从能跑到可用的关键设计
在实际的 AI 应用开发中Agent智能体已经从概念验证走向业务落地。过去一段时间里LangChain、LlamaIndex、AutoGen、CrewAI 等框架把 Agent 的搭建成本压得越来越低定义一个工具列表、写一个 system prompt、调用几次大模型一个能查天气、能查库存、能发邮件的 Agent 就能跑起来。真正让项目停滞的地方反而转移到另一个问题当一轮对话、一次任务需要跨越十几个工具调用、上下几十轮交互时Agent 怎么知道自己刚才做过什么、用户真正想要什么、哪些信息还能信任。这就是上下文引擎Context Engine要解决的问题。下面从一个可运行的最小项目出发拆解上下文引擎的核心设计并给出一套可复现的代码、排查路径和落地清单。全文分为七个部分先解释为什么 Agent 搭建不再难再定义上下文引擎的边界接着实现一个最小上下文引擎然后处理上下文溢出与遗忘再谈多 Agent 协作中的上下文传递最后给常见问题排查和最佳实践。1. Agent搭建为什么已经从技术难点变成基础能力1.1 从手写循环到框架化先回到 Agent 的本质。一个 Agent 通常由四部分组成模型Model、指令Instruction、工具Tools和循环Loop。在没有框架的年代开发者要自己写一个 ReAct 风格的循环把用户问题拼进 prompt调用模型让模型决定是回答还是调用工具调用工具后把结果拼回去再调用模型直到模型认为可以给出最终答案。这个循环本身并不复杂但工程化之后问题很多工具返回的结果格式不统一怎么办模型陷入反复调用同一个工具怎么办一次任务超过模型上下文上限怎么办并发任务如何隔离状态。于是各种 Agent 框架开始把这些问题统一处理。现在的典型写法是这样from agent_framework import Agent, Tool def query_stock(code: str) - str: # 实际项目中这里会调外部接口 return f{code} 最新价格 12.35 agent Agent( namestock-assistant, modelyour-model-name, tools[Tool.from_function(query_stock)], system_prompt你是一个股票助手使用工具回答股票问题。, ) result agent.run(查询 600519 的价格) print(result)这段代码今天已经能跑通绝大多数 Agent 框架。它说明一个问题Agent 的“搭建”环节包括工具注册、模型选择、任务编排已经被框架标准化了。团队之间比拼的不再是谁能写出 Agent而是谁能写出稳定、可控、可运维的 Agent 系统。1.2 框架帮你解决什么没帮你解决什么框架确实替你处理了基础链路但下面这些任务仍然要自己设计多轮对话历史如何存储、裁剪、压缩。长期信息如何从数据库、文档、知识库中召回。多个工具之间的中间结果如何传递。一个 Agent 出现异常时如何回滚或重试。多个 Agent 协作时共享上下文和隐私边界如何划分。这些都不是“加一个依赖”就能解决的它们共同指向同一个底层能力上下文引擎。为了说清楚框架的边界可以用一句话概括框架解决“Agent 怎么跑”上下文引擎解决“Agent 记得什么、看得到什么、依据什么做判断”。维度Agent 框架上下文引擎主要职责工具调用、任务编排、模型接入上下文存储、裁剪、压缩、检索典型组件Agent、Tool、WorkflowContext Manager、Memory、Retriever出问题时的表现工具调用失败、流程中断上下文溢出、遗忘、串场、胡编能否直接复用可以直接换框架需要结合业务设计2. 理解上下文引擎Agent的“工作记忆”到底指什么2.1 上下文不只是一个 prompt 字符串很多开发者在第一次搭建 Agent 时把“上下文”理解为 system prompt 加上几轮对话。这种理解在演示项目里够用进到生产环境就出问题。上下文应该分成三个层次用户层上下文用户本次会话的诉求、偏好、身份信息、历史交互。任务层上下文当前任务的目标、已执行步骤、工具返回结果、中间状态。知识层上下文长期记忆、企业知识库、文档资料、历史相似案例。这三个层次最后都要在调用模型时拼成一段可用的文本。上下文引擎的作用就是在每个层次做好提取、存储、筛选和组合保证每次调用模型时模型能看到“当前最需要的、可信的信息”而不是“所有能塞进窗口的信息”。2.2 上下文窗口、Token 预算和 Agent 状态模型一次能处理的最大 token 数叫上下文窗口。常见的模型窗口在 8k 到 200k 之间。窗口越大能塞的历史越多但会有三个成本成本按 token 计费输入越多越贵。延迟输入 token 越多首 token 延迟越明显。噪声无关信息越多模型越容易丢失重点甚至被历史中的错误带偏。所以上下文引擎的第一件事是管理 Token 预算。它的核心语义是在有限的窗口里优先保留对当前决策最有用的信息。class ContextBudget: def __init__(self, max_tokens: int, reserved_output: int): self.max_tokens max_tokens self.reserved_output reserved_output property def input_budget(self) - int: # 给模型输出预留空间输入可用的 token 上限放在这里统一控制 return self.max_tokens - self.reserved_output这里的reserved_output很关键。很多上下文溢出问题不是输入太长而是输入把整个窗口占满模型根本没有空间生成回复。生产环境通常要预留 20% 到 30% 的输出空间。注意上下文管理的目标不是“尽量多塞信息”而是“在有限窗口里保留对当前决策最有用的信息”。2.3 为什么说上下文引擎是破局关键现在市面上的 Agent 项目越来越多但真正被用户投诉的问题集中在三类问了几轮之后Agent 把用户最早的需求忘了。Agent 在长文档场景下开始复述错误信息。多轮工具调用后结果相互矛盾。这三类问题没有一个是“模型不够聪明”造成的都是上下文管理不到位。换句话说Agent 的上限由模型决定Agent 的下限由上下文引擎决定。把上下文引擎做好是让 Agent 从“能跑”走向“可用”的关键一步。3. 最小上下文引擎实战从零构建一个带记忆的 Agent3.1 项目结构与环境准备下面用一个最小项目演示上下文引擎的完整实现。项目使用 Python 3.10只依赖一个 OpenAI 兼容的客户端接口和一个可选的分词库。为了便于替换这里写成通用结构。context_agent/ ├── agent.py # Agent 主循环 ├── context_manager.py # 上下文管理器Token 预算、裁剪、压缩 ├── memory_store.py # 记忆存储短期 长期 ├── config.py # 配置 └── requirements.txtrequirements.txt 内容如下openai1.0 tiktoken0.5如果使用的是国内大模型厂商提供的 OpenAI 兼容接口只需要在客户端里修改 base_url 和 api_key。下面示例中的模型名称要替换成你自己能访问的模型。3.2 实现上下文管理器上下文管理器负责维护一份消息列表并确保调用模型前消息总长度不超过预算。# context_manager.py from typing import List, Dict def count_tokens(text: str, model: str default) - int: 估算 token 数。没有 tiktoken 时退化为字符数除以 3。 try: import tiktoken encoding tiktoken.encoding_for_model(model) return len(encoding.encode(text)) except Exception: return len(text) // 3 class ContextManager: def __init__(self, max_tokens: int 8000, reserved_output: int 2000): self.max_tokens max_tokens self.reserved_output reserved_output self.messages: List[Dict[str, str]] [] def input_budget(self) - int: return self.max_tokens - self.reserved_output def used_tokens(self) - int: return sum(count_tokens(m[content]) for m in self.messages) def remaining(self) - int: return self.input_budget() - self.used_tokens() def add_message(self, role: str, content: str) - None: self.messages.append({role: role, content: content}) def build_payload(self) - List[Dict[str, str]]: # 实际项目中先裁剪再返回这里先返回原始消息 return list(self.messages)这里把 Token 估算放在单独函数里便于后续替换成更精确的计数方式。实际项目中不同模型的分词器不一致建议在部署前用目标模型的分词器做一次校准。3.3 实现记忆分层短期记忆与长期记忆记忆分层的目标是把“最近要用的”和“长期要用的”分开管理。短期记忆直接放在上下文管理器里对应最近几轮对话。长期记忆存放在外部存储里每次任务开始前按相关性召回。# memory_store.py import json import os class MemoryStore: 一个极简长期记忆存储按 key 写入文件。 生产环境通常会换成向量数据库或关系数据库这里只演示接口设计。 def __init__(self, path: str ./memory.json): self.path path self.data: dict {} if os.path.exists(path): with open(path, r, encodingutf-8) as f: self.data json.load(f) def save(self, key: str, content: str) - None: self.data[key] content with open(self.path, w, encodingutf-8) as f: json.dump(self.data, f, ensure_asciiFalse, indent2) def search(self, keyword: str, limit: int 3) - list: 极简召回包含关键词就返回。生产环境建议用向量检索。 results [] for key, content in self.data.items(): if keyword in key or keyword in content: results.append(content) return results[:limit]这种“按关键词召回”的方式只用于演示接口生产环境要做语义召回一般流程是把用户当前问题向量化在向量数据库中检索最相似的记忆片段再拼进上下文。3.4 把上下文引擎接入 Agent 主循环下面是 Agent 主循环。它做的事情是接收用户输入先从长期记忆中召回相关片段把片段作为 system 前缀加入上下文调用模型把用户消息和模型回复都写入短期上下文最后把关键结论写入长期记忆。# agent.py from context_manager import ContextManager from memory_store import MemoryStore class ContextAgent: def __init__(self, llm_client, model: str, system_prompt: str): self.llm_client llm_client self.model model self.system_prompt system_prompt self.context ContextManager() self.memory MemoryStore() def run(self, user_input: str) - str: # 1. 召回长期记忆 memory_snippets self.memory.search(user_input) system self.system_prompt if memory_snippets: system \n\n相关历史信息\n \n.join(memory_snippets) # 2. 组装消息 self.context.add_message(user, user_input) payload [{role: system, content: system}] self.context.build_payload() # 3. 调用模型 response self.llm_client.chat.completions.create( modelself.model, messagespayload, ) answer response.choices[0].message.content # 4. 写回短期上下文 self.context.add_message(assistant, answer) # 5. 写长期记忆这里用用户输入做 key 演示 self.memory.save(user_input, answer) return answer这个示例里有几个明显的简化没有做上下文裁剪长期记忆是同步写文件没有处理工具调用。它的价值在于展示上下文引擎的四个动作召回、组合、写回、持久化。真实项目要在此基础上完善裁剪策略和检索策略。4. 上下文检索与压缩让长会话不崩4.1 滑动窗口截断最直观的策略是只保留最近 N 轮对话。对于简单的客服场景这种方案足够稳定实现成本最低。def truncate_by_window(messages, keep_rounds: int 10): user_msgs [m for m in messages if m[role] user] if len(user_msgs) keep_rounds: return messages # 保留最后 keep_rounds 轮按 user 消息位置定位 start_idx len(user_msgs) - keep_rounds seen_user 0 cut_index 0 for i, m in enumerate(messages): if m[role] user: seen_user 1 if seen_user start_idx: cut_index i break return messages[cut_index:]滑动窗口的缺点也很明显最早的关键信息会被直接丢掉。它适合对历史依赖不强的场景例如单轮问答、简单工具调用。4.2 摘要压缩当早期信息不能丢时用摘要代替原文。做法是当上下文即将超限时把最早的消息交给模型压缩成一段摘要替换掉原文。def compress_early_messages(llm_client, model, messages, keep_last: int 6): early messages[:-keep_last] recent messages[-keep_last:] text \n.join(f{m[role]}: {m[content]} for m in early) summary llm_client.chat.completions.create( modelmodel, messages[ { role: system, content: 把以下对话历史压缩成一段 200 字以内的摘要保留关键事实、用户偏好和未完成事项。, }, {role: user, content: text}, ], ).choices[0].message.content return [{role: system, content: f历史摘要{summary}}] recent摘要压缩的关键是摘要质量。如果摘要丢失了用户未完成事项后续任务就会脱节。因此压缩 prompt 里要明确要求保留“未完成事项”和“关键约束”。4.3 向量检索召回向量检索用于长期记忆和知识库场景。它的思路是不把所有历史都放进上下文而是根据当前问题从知识库中召回最相关的片段。典型流程把文档切分成固定大小片段比如 512 token 一段。对每个片段生成向量写入向量数据库。用户提问时对问题生成向量检索 Top-K 相似片段。把召回的片段拼入上下文再调用模型。def retrieve_snippets(query: str, vector_store, top_k: int 5) - list[str]: query_vec embed(query) results vector_store.search(query_vec, top_ktop_k) return [r.text for r in results]这里embed和vector_store只是接口占位生产环境可以是任意嵌入模型和三方向量数据库。要注意检索效果依赖文本切分质量切片太碎容易丢失语义太大则召回不精准。一般从 256 到 1024 token 之间做实验确定。5. 上下文在多 Agent 协作中的传递与隔离5.1 主从模式下的上下文传递多 Agent 协作是最近讨论很多的方向。常见的主从模式中主 AgentSupervisor负责任务拆解子 AgentSubagent负责具体执行。很多人纠结子 Agent 和工具调用的区别一个实用的理解是在主从模式下子 Agent 本质上是另一种形式的工具——它接受输入、返回结果只是内部实现是一个完整的 Agent 循环。这种模式下上下文传递要遵循一个原则主 Agent 只传任务上下文不传全部记忆。否则每个子 Agent 都背着整条对话历史成本高且任务容易发散。def run_subagent(sub_agent, task: str, relevant_context: dict) - str: return sub_agent.run( tasktask, initial_contextrelevant_context, )relevant_context应该是从主上下文里筛选出来的、与本任务相关的片段而不是全部消息。5.2 Skill 与 MCP工具上下文如何进入 Agent在 Agent 生态里Skill 和 MCPModel Context Protocol经常被同时提起。两者的区别可以用一句话概括Skill 是“Agent 会做什么”的封装告诉 Agent 一组行为和输入输出约定MCP 是“工具怎么被统一接入”的协议让不同 Agent 框架可以用同一种方式调用外部能力。它们不是对立关系Skill 可以基于 MCP 协议实现。这两个概念对上下文引擎的意义在于每个工具、每个 Skill 都会产生上下文。工具返回结果、错误信息、执行状态都应该有统一的格式和生命周期。否则 Agent 会把上一次工具报错当成有效信息继续使用。5.3 上下文安全与权限边界多 Agent 场景下上下文隔离同时是安全问题。一个子 Agent 只应该看到完成自己任务所必需的信息。设计上下文引擎时可以在消息层加来源标签{role: user, content: ..., source: customer-profile}然后在组装给子 Agent 的消息时按权限过滤来源。这样能避免用户 A 的数据被子 Agent 写入用户 B 的上下文也是生产环境合规审计的基础。6. 常见问题排查上下文溢出、遗忘和串场6.1 Agent 执行超时the agent execution provider did not respond in time在部分 Agent 框架的日志里会出现类似“the agent execution provider did not respond in time”的报错。它字面意思是执行器没有在预期时间内返回结果。常见原因有三类上下文过长导致模型推理时间超过执行器超时阈值。外部工具接口响应缓慢拖慢了整体执行。执行器配置的超时时间太短模型本身正常但被误判。排查顺序建议先看日志中该次执行的 token 数和耗时判断是否上下文过大。再看工具调用耗时定位是哪一个工具拖慢了整体。最后检查执行器超时配置确认是否过小。处理方式对应为缩短上下文、给工具调用加独立超时、适当调大执行器超时并加重试。预防这类问题应该在 Agent 启动前就设置好上下文预算并给工具调用设置兜底超时。排查执行超时问题时先看 token 数和耗时拆分不要一上来就调大超时。超时调大只能掩盖问题不能解决上下文过长或工具过慢的根源。6.2 对话多轮后 Agent 忘了早期信息现象用户在第 15 轮询问“我最早提到的那个需求你记得吗”Agent 回答不了。可能原因滑动窗口把早期消息截掉了。早期信息没有写入长期记忆。摘要压缩丢失了关键约束。检查方式打印上下文管理器里的实际 messages确认早期消息是否还在。如果还在但模型答不上来可能是信息在太多内容中被稀释需要把重要信息放到离问题更近的位置。修复建议对重要信息建立“钉住”机制例如把关键用户偏好始终放在 system prompt 的固定位置不允许被裁剪。问题现象常见原因检查方式处理建议多轮后遗忘早期需求滑动窗口截断打印 messages 查看长期记忆落库 关键信息钉住长文档回答前后矛盾上下文噪声过多查看输入 token 和片段顺序检索召回 相关性排序工具报错被当成有效结果工具输出无状态标记检查工具返回格式统一工具结果 schema执行超时上下文过长或工具慢查看耗时拆分裁剪上下文 工具超时兜底6.3 不同任务的上下文互相污染现象用户开了两个不同的任务Agent 把任务 A 的信息带到任务 B 的回答里。原因通常是上下文管理器是全局单例多个会话共用一份消息列表。解决方案按会话 ID 隔离上下文。self.contexts: dict[str, ContextManager] {} def get_context(self, session_id: str) - ContextManager: if session_id not in self.contexts: self.contexts[session_id] ContextManager() return self.contexts[session_id]这也解释了为什么上下文引擎要独立于 Agent 框架设计框架通常不关心会话级别的状态隔离而这恰恰是生产环境最容易出问题的点。7. 最佳实践与落地清单7.1 上下文引擎设计清单在实现自己的上下文引擎时可以按下面的清单做一轮自检是否设置了输入 Token 预算并且预留了输出空间。是否区分了用户层、任务层、知识层上下文。是否支持按会话 ID 隔离上下文。是否定义了长期记忆的写入时机和召回策略。是否对工具返回结果做了结构化 schema。是否对重要信息有“钉住”机制。是否在调用模型前对上下文总量做兜底截断。是否记录了每次调用的 token 消耗和耗时。7.2 学习环境与生产环境的差异学习环境里把上下文存进内存、用关键词召回、把所有内容塞进窗口都能跑通。生产环境要额外处理配置外置化模型名、窗口上限、超时时间放到配置中心。持久化上下文和记忆放到 Redis 或数据库中重启不丢。监控记录 token 消耗、上下文命中率、工具调用成功率。安全对来源标签做权限过滤防止跨会话数据泄露。回滚工具调用失败时能从最近一个稳定状态重试。7.3 下一步扩展方向上下文引擎还有几个值得深入的方向。第一语义记忆替换关键词召回引入嵌入模型和向量数据库。第二为不同任务设计独立的上下文策略例如简单问答用滑动窗口复杂任务用摘要压缩加固定系统信息。第三把上下文引擎与可观测系统打通通过日志还原“模型当时看到了什么”这是排查 Agent 幻觉和错误决策最有效的手段。对新手而言最有价值的练习不是换更多框架而是用一个模型 API 和一份本地 JSON把上面的最小上下文引擎逐行实现一遍然后故意制造上下文溢出观察模型在什么情况下开始遗忘和反复。理解了这些问题再进入多 Agent 和检索增强会顺畅很多。
