先说一个业内越来越明显的共识:Agent 搭建本身已经不再是难点。
现在无论是开源框架还是商业化平台,LangChain、Dify、Coze、FastGPT 等方案已经相当成熟。配合一个可用的 LLM API,几十行代码就能跑起一个会调用工具、能查资料、能写代码的 Agent。不少开发者甚至把 ReAct、Plan-and-Execute、多 Agent 协作这些模式都玩得很熟。
但很多团队在做完 Demo 之后,迟迟无法把 Agent 放到生产环境。踩过的坑集中在几个地方:对话一长效果就变差、Agent 会遗忘前几轮的任务目标、工具调用状态经常错乱、Token 消耗飙升。这些问题表面上看是模型能力不够,实际上是上下文管理环节出了问题。
本文将围绕“Agent 搭建 + 上下文引擎”展开。先讲清楚为什么 Agent 搭建不再是难点,再重点拆解上下文引擎的定位、组成和实现思路,最后通过一个从裸 Agent 到完整上下文引擎的实战案例,带你理解如何把 Agent 从“能跑 Demo”推进到“适合生产落地”。文章同时会覆盖多 Agent 协作下的上下文设计、常见问题排查和工程化建议,适合正在学习 Agent 开发的初学者,也适合已经在做 Agent 应用落地的开发者参考。
1. Agent搭建为什么不再是难题
1.1 框架和平台逐步成熟
早期做 Agent,很多团队是从零开始写模型调用、工具解析、记忆管理、任务编排。重复造轮子不说,还容易在各种边界条件上翻车。比如模型返回的 JSON 不合法、工具参数解析失败、多轮对话历史越积越长导致请求超时。
现在的情况不同了。框架层面有LangChain、LlamaIndex、AutoGen、CrewAI等;平台层面有Dify、Coze、FastGPT等。这些工具把下面几件事都做了:
- 模型接入。
- 工具/函数调用协议。
- 常用 Agent 模式(ReAct、Plan-and-Execute、Reflection)。
- 向量库、记忆组件。
- 可视化编排和日志。
换句话说,Agent 的基础设施已经“水电煤”化。你要做一个能回答问题的 Agent,核心代码量可以压缩到很少。对于有经验的后端开发者来说,把一个 LLM API 接进来、注册几个工具函数、跑一个循环,是半天内就能完成的事情。
1.2 一个最简单的Agent循环
为了让读者对“Agent 搭建不难”有直观感受,下面先看一个极简的 Agent 循环。这个示例没有引入任何 Agent 框架,只依赖模型 API 和一段循环逻辑。
# 文件路径:demo/minimal_agent.py # 说明:假设你已经配置好 OPENAI_API_KEY, 并且使用 OpenAI 兼容接口 from openai import OpenAI client = OpenAI() TOOLS = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名"} }, "required": ["city"] } } } ] def get_weather(city: str) -> str: # 这里只是演示,可以替换成真实天气 API return f"{city} 今天晴,气温 24℃,适合外出。" def run_agent(user_input: str): messages = [ {"role": "system", "content": "你是一个智能助手,可以调用工具帮助用户。"}, {"role": "user", "content": user_input} ] for _ in range(5): resp = client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=TOOLS, ) msg = resp.choices[0].message if not msg.tool_calls: # 没有工具调用,说明 Agent 已经给出最终答案 print(msg.content) return messages.append(msg) for tool_call in msg.tool_calls: args = eval(tool_call.function.arguments) tool_name = tool_call.function.name result = get_weather(**args) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result }) print("超出最大循环次数") if __name__ == "__main__": run_agent("北京天气怎么样?")在这个示例里,Agent 的本质其实就是:
- 把用户输入、系统提示词、历史消息、工具定义一起发给模型。
- 模型决定是直接回答,还是调用某个工具。
- 如果调用工具,就把“调用参数 + 工具返回结果”拼回消息列表。
- 继续让模型决策,直到模型给出最终答案。
这个循环并不复杂。真正复杂的是:当会话变得很长、工具变得很多、任务链变得很深时,如何让模型始终记得最关键的信息,而不被冗余历史带偏。
1.3 真正的分水岭:上下文
很多团队早期都会做一个类似的判断:先跑通 Agent 循环,再慢慢补上下文。但实际进入生产后会发现,上下文不是“补丁”,而是 Agent 的骨架。
一个典型的场景是这样的:
用户在第一轮告诉 Agent:“帮我分析这份销售数据,重点关注华东区。” 第二轮说:“再对比一下上个月的环比。” 第三轮说:“把结论用表格输出,并给出下一步建议。”
如果 Agent 没有保存好第一轮的“华东区”约束,第三轮很可能会输出全国数据;如果 Agent 没有保留之前的分析结论,第三轮可能会重新读一遍数据,Token 消耗成倍增长。这些都不是模型“笨”,而是上下文没有被正确组织。
所以从工程视角看,Agent 开发的核心难点正从“搭建链路”转移到“上下文引擎”。
2. 上下文引擎:Agent的“工作记忆”
2.1 上下文引擎是什么
先给一个通俗理解:上下文引擎可以类比人的“工作记忆 + 笔记系统”。它不仅保存对话内容,还负责以下几件事:
- 判断哪些信息对当前任务重要。
- 把重要信息组织成模型容易理解的格式。
- 当上下文超过模型窗口限制时,做压缩或裁剪。
- 在需要时,从更早的对话或知识库中检索出相关内容。
- 记录工具调用、任务状态等过程性信息。
专业一点说,上下文引擎是围绕大模型上下文窗口构建的一套管理机制,通常包括:短期对话管理、长期记忆管理、上下文压缩策略、向量检索召回、Token 预算控制,以及与工具调用状态同步的能力。
可以这样理解分层关系:
用户请求 ↓ 上下文引擎 ├── 短期上下文(当前会话消息、工具调用栈) ├── 压缩层(摘要、滑动窗口) ├── 长期记忆层(向量检索、结构化记忆) ├── 任务状态层(目标、子任务、决策记录) └── 上下文回放/日志 ↓ 组装后的完整 Prompt,发送给 LLM2.2 上下文困境的三个层面
先看模型层面的困境。大模型的上下文窗口虽然越来越大,但“窗口大”不等于“效果好”。业界有一个共识:**在超长上下文中,模型对中间部分信息的关注度会下降,也更容易被无关信息干扰。**如果你把所有历史消息原封不动塞给模型,往往会出现两个问题:关键信息被稀释,Token 成本快速上涨。
再看工程层面的困境。Agent 不是只有一轮问答,它会经历“理解目标 → 制定计划 → 调用工具 → 观察结果 → 修正计划 → 输出结论”的循环。每个环节都会产生中间状态。如果中间状态没有被上下文引擎统一管理,一旦某次工具调用返回格式异常或中断,整个 Agent 状态就会丢失。这也是“Agent 执行不稳定”的典型来源。
最后是组织层面的困境。在多 Agent 协作场景里,主 Agent 和子 Agent 各自维护什么上下文、共享什么上下文、如何避免上下文污染,如果没有一套清晰的引擎设计,很容易变成“多个 Agent 各自为战,整体效果还不如单 Agent”。
2.3 上下文引擎的组成部分
从上文可以总结出,上下文引擎通常由五个核心部分组成:
| 组成部分 | 作用 | 典型实现 |
|---|---|---|
| 短期记忆 | 保存当前会话最近的多轮消息 | 内存消息队列、Redis |
| 上下文压缩 | 控制 Token 预算,防止上下文无限膨胀 | 滑动窗口、摘要生成 |
| 长期记忆 | 保存用户偏好、历史事实、任务结论 | 向量库、KV 存储 |
| 检索召回 | 从长期记忆中找出与当前问题相关的内容 | Embedding + 向量检索 |
| 状态同步 | 记录工具调用链、任务进度、中间结果 | 结构化事件日志 |
在实际项目中,这五部分不一定全部要自研。很多框架已经提供了基础组件,但如何组织它们、压缩策略怎么定、什么时候触发摘要、记忆怎么写入和召回,仍然要结合业务场景仔细设计。这就是“上下文引擎”值得单独拿出来研究的原因。
3. 环境准备与项目结构
3.1 依赖与运行环境
本文实战示例使用 Python。版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。
- 操作系统:Windows / macOS / Linux 均可。
- Python 版本:建议 3.10 或更高。
- 模型服务:一个支持 OpenAI 兼容协议的大模型 API,例如 OpenAI 官方接口,或其他兼容接口。你只需要替换
base_url和api_key。 - 核心依赖:
openaiPython 包。
安装依赖:
pip install openai如果你还没有可用的模型 API,也可以用本地模型或国内模型平台提供的 OpenAI 兼容接口。关键是理解代码中的“接口关注点”,而不是依赖某一个具体厂商。
3.2 项目目录规划
为了让后续代码清晰,我们按下面的目录组织工程。没有引入复杂框架,所有组件都是轻量实现,便于看清楚上下文引擎的每个齿轮。
context_engine_demo/ ├── main.py # 入口,演示完整流程 ├── llm_client.py # 模型调用封装 ├── context_engine.py # 上下文引擎核心 ├── memory.py # 记忆存储与检索 ├── tools.py # 工具定义与实现 └── config.py # 配置后续所有代码片段都会标注文件路径。
4. 实战:一步步把上下文引擎做出来
这一节是核心。我们从一个“裸 Agent”开始,逐步加入上下文引擎的各个能力。每加一块,都解释为什么要这么做。
4.1 第一阶段:裸Agent与它的短板
先写一个最简单的多轮对话 Agent。它没有上下文引擎,只是机械地拼接消息。
# 文件路径:context_engine_demo/llm_client.py from openai import OpenAI client = OpenAI() def chat(messages, model="gpt-4o-mini", temperature=0.7): resp = client.chat.completions.create( model=model, messages=messages, temperature=temperature, ) return resp.choices[0].message.content然后写一个最原始的对话循环:
# 文件路径:context_engine_demo/main.py from llm_client import chat history = [] while True: user_input = input("用户: ") if user_input.lower() in ("exit", "quit"): break history.append({"role": "user", "content": user_input}) reply = chat(history) history.append({"role": "assistant", "content": reply}) print("Agent:", reply)这个版本的问题很明显:
- 历史消息无限增长,最终会超过模型上下文窗口。
- 每当用户新输入一句话,整个历史都会被重新发送,Token 成本不断上升。
- 没有摘要能力,早期的重要内容会被越来越多的无关消息淹没。
- 没有工具调用记录,Agent 无法感知“我已经查过天气”这类中间状态。
4.2 第二阶段:上下文管理器
第二版加入一个ContextEngine类。它负责统一保存消息、提供构建 Prompt 的入口,为后面的压缩和检索留好扩展点。
# 文件路径:context_engine_demo/context_engine.py class ContextEngine: def __init__(self, system_prompt: str): self.system_prompt = system_prompt self.short_term = [] # 短期对话列表 self.summary = "" # 历史摘要 self.memory_items = [] # 长期记忆条目,实际项目可替换为向量库 def add_user_message(self, content: str): self.short_term.append({"role": "user", "content": content}) def add_assistant_message(self, content: str): self.short_term.append({"role": "assistant", "content": content}) def add_tool_result(self, tool_call_id: str, content: str): self.short_term.append({ "role": "tool", "tool_call_id": tool_call_id, "content": content }) def build_messages(self): messages = [{"role": "system", "content": self.system_prompt}] # 先放摘要,让模型知道更早的对话发生了什么 if self.summary: messages.append({ "role": "system", "content": f"以下是更早对话的摘要,可作背景参考:\n{self.summary}" }) # 再放检索到的长期记忆 if self.memory_items: memory_text = "\n".join( f"- {item['content']}" for item in self.memory_items[-3:] ) messages.append({ "role": "system", "content": f"以下是长期记忆中与当前任务可能相关的信息:\n{memory_text}" }) messages.extend(self.short_term) return messages这个类虽然没有引入复杂的压缩策略,但已经明确了上下文引擎的分层结构:系统提示词、摘要、长期记忆、短期对话。后续要做的压缩和检索都是在这些位置上做文章。
4.3 第三阶段:滑动窗口与Token预算
接下来解决第一个硬问题:上下文无限增长。一个实用策略是滑动窗口,也就是只保留最近 N 条消息。更精细的方式是估算 Token 数量,当消息总长度超过预算时,从最旧的非系统消息开始裁剪。
# 文件路径:context_engine_demo/context_engine.py def estimate_tokens(text: str) -> int: # 粗略估算。英文约 4 字符/token,中文约 1.5~2 字符/token。 # 实际项目中可以接入 tokenizer 精确计算。 return max(1, len(text) // 2) class ContextEngine: def __init__(self, system_prompt: str, max_context_tokens: int = 4000, max_messages: int = 20): self.system_prompt = system_prompt self.short_term = [] self.summary = "" self.memory_items = [] self.max_context_tokens = max_context_tokens self.max_messages = max_messages def _trim_by_count(self): """先按消息条数限制滑动窗口""" if len(self.short_term) > self.max_messages: # 保留最近 max_messages 条消息 self.short_term = self.short_term[-self.max_messages:] def _trim_by_tokens(self): """再按 Token 预算裁剪,保证组装后的消息不会超限""" while self._current_tokens() > self.max_context_tokens and len(self.short_term) > 0: # 丢弃最旧的非 system 消息 self.short_term.pop(0) def _current_tokens(self) -> int: return sum(estimate_tokens(m.get("content") or "") for m in self.short_term) def _compress_if_needed(self): # 摘要模块后面实现,先留占位 pass def build_messages(self): self._trim_by_count() self._trim_by_tokens() self._compress_if_needed() messages = [{"role": "system", "content": self.system_prompt}] if self.summary: messages.append({ "role": "system", "content": f"以下是更早对话的摘要,可作背景参考:\n{self.summary}" }) if self.memory_items: memory_text = "\n".join( f"- {item['content']}" for item in self.memory_items[-3:] ) messages.append({ "role": "system", "content": f"以下是长期记忆中与当前任务可能相关的信息:\n{memory_text}" }) messages.extend(self.short_term) return messages这里的重点是:当上下文超过预算时,不能直接清空,而是保留最近的信息,因为对话中“最近的指令”通常优先级最高。这个策略很简单,但在大多数场景下能明显提升长对话的稳定性。
4.4 第四阶段:摘要记忆
只做滑动窗口会带来一个新问题:早期的重要约束可能会被丢弃。比如用户第一轮指定“只分析华东区”,如果这条指令被滑出窗口,后面的回复就可能出偏差。
解决办法是摘要记忆。当消息超过一定数量时,把即将被丢弃的旧消息压缩成一段摘要,存到summary字段。
# 文件路径:context_engine_demo/context_engine.py import json from llm_client import chat class ContextEngine: # 省略前面的部分,只展示新增摘要逻辑 def _compress_if_needed(self): # 当短期消息数量超过阈值时,触发一次摘要 if len(self.short_term) < self.max_messages: return to_compress = self.short_term[:-10] # 保留最近 10 条 self.short_term = self.short_term[-10:] # 将旧消息整理成摘要 dialog_text = json.dumps(to_compress, ensure_ascii=False) prompt = ( "请把下面的对话整理成一段简洁摘要。" "需要保留:用户的核心目标、约束条件、已确认的事实、未完成的任务。\n\n" f"{dialog_text}" ) new_summary = chat([{"role": "user", "content": prompt}]) # 如果已有摘要,需要和新的摘要合并 if self.summary: merge_prompt = ( f"下面有两段关于同一对话的摘要,请合并成一段,去掉重复信息:\n" f"旧摘要:{self.summary}\n" f"新摘要:{new_summary}" ) self.summary = chat([{"role": "user", "content": merge_prompt}]) else: self.summary = new_summary摘要生成本身也要消耗 Token,所以触发时机需要控制。通常不建议每条消息都触发,而是在即将裁剪之前触发一次。生产环境中还可以用更轻量的摘要模型,或直接用规则抽取关键信息。
4.5 第五阶段:可检索的长期记忆
摘要解决了“对话内遗忘”,但无法解决“跨会话记忆”。例如用户今天让 Agent 记住“我常用华东区数据”,明天新开一个会话,这个信息应该仍然有效。
生产环境通常会使用向量数据库。把历史关键信息写入向量库,每次组装 Prompt 前,用“当前问题 + 最近对话”去检索最相关的记忆。为了不让示例依赖外部服务,这里先用一个简单的内存实现,但会说明它与真实向量库的对应关系。
# 文件路径:context_engine_demo/memory.py import re class MemoryStore: def __init__(self): self.items = [] # 每个元素形如 {"content": str, "embedding": vector} def add(self, content: str): # 真实项目会把 content 向量化后存到 Chroma / pgvector / Milvus self.items.append({"content": content}) def search(self, query: str, top_k: int = 3): # 简化检索:用关键词重合程度作为相似度 # 真实项目使用 embedding 向量余弦相似度 if not query or not self.items: return [] query_tokens = set(re.findall(r"\w+", query.lower())) scored = [] for item in self.items: content_tokens = set(re.findall(r"\w+", item["content"].lower())) if len(query_tokens) == 0 or len(content_tokens) == 0: score = 0 else: intersec = len(query_tokens & content_tokens) union = len(query_tokens | content_tokens) score = intersec / union if union > 0 else 0 scored.append((score, item)) scored.sort(key=lambda x: -x[0]) return [item for score, item in scored[:top_k] if score > 0]在ContextEngine中接入 MemoryStore。当一轮对话结束,或者 Agent 确认了某个事实后,写一条记忆;下次构建上下文时,从 MemoryStore 检索相关记忆。
# 文件路径:context_engine_demo/context_engine.py from memory import MemoryStore class ContextEngine: def __init__(self, system_prompt: str): # 省略其他初始化 self.memory_store = MemoryStore() def add_memory(self, content: str): # 真实场景会先判断这条信息是否有长期保留价值 self.memory_store.add(content) def recall_memory(self, query: str, top_k: int = 3): return self.memory_store.search(query, top_k=top_k) def build_messages(self, query: str): # 检索并注入长期记忆 self.memory_items = self.recall_memory(query, top_k=3) # 其余逻辑与之前一致 ...这里有一个容易被忽视的细节:不是所有对话内容都值得写入长期记忆。用户闲聊、临时指令、错误猜测都不应该污染记忆库。生产环境中建议通过规则或一个专门的“记忆抽取模型”来过滤。
4.6 第六阶段:工具调用状态的完整保留
Agent 与普通聊天机器人的最大区别是“会调用工具”。工具调用会引入新的状态:模型输出 tool_calls、系统执行工具、再把结果回传。这个过程中只要少了一步,Agent 的上下文就是断裂的。
下面给出一种标准的保存方式,适合 OpenAI 兼容接口的函数调用协议:
# 文件路径:context_engine_demo/main.py import json from context_engine import ContextEngine from llm_client import chat from tools import get_weather TOOL_FUNCTIONS = { "get_weather": get_weather, } def run_turn(engine: ContextEngine, user_input: str): engine.add_user_message(user_input) messages = engine.build_messages() resp = client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=TOOL_SCHEMAS, ) msg = resp.choices[0].message # 工具调用需循环执行 while msg.tool_calls: messages.append(msg) for tool_call in msg.tool_calls: tool_name = tool_call.function.name args = json.loads(tool_call.function.arguments) result = TOOL_FUNCTIONS[tool_name](**args) # 同时保存到 short_term 和消息列表 engine.add_tool_result(tool_call.id, json.dumps(result, ensure_ascii=False)) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False) }) resp = client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=TOOL_SCHEMAS, ) msg = resp.choices[0].message reply = msg.content engine.add_assistant_message(reply) return reply核心原则是:tool_call 和 tool result 必须成对保存在上下文中。如果工具调用已经执行,但后续某轮 Prompt 构建时把 tool result 丢掉了,模型就会以为自己还没查过数据,从而重复调用工具,或者做出错误判断。
5. 多Agent协作下的上下文设计
5.1 主从模式的上下文关系
当任务足够复杂时,一个 Agent 往往搞不定。常见方案是主从模式:主 Agent 负责任务拆解和最终汇总,sub-agent 负责具体子任务。
这种模式下,上下文不能简单共享同一个列表。否则多个子任务的中间过程会互相干扰。更合理的做法是:
- 主 Agent 持有一个“全局上下文”,记录用户目标、子任务列表、关键决策。
- 每个 sub-agent 持有独立的“局部上下文”。
- sub-agent 执行完任务后,向主 Agent 返回一段“结果摘要”,而不是把所有原始上下文都带回。
用户目标 ↓ 主 Agent(全局上下文) ├── 任务拆解 ├── 派发 sub-agent A(局部上下文 A) ├── 接收 A 的结果摘要 ├── 派发 sub-agent B(局部上下文 B) └── 汇总所有结果,输出最终答案5.2 sub-agent 本质上是另一种 tool
在很多现代 Agent 框架里,sub-agent 的调用方式和函数调用非常相似。你可以把一个 sub-agent 看成是一个“特殊的工具”:输入是一段任务描述,输出是一个结构化结果。这样做的好处是:
- 主 Agent 不需要关心 sub-agent 内部用了多少轮推理、调用了哪些工具。
- sub-agent 的上下文可以被隔离,避免主 Agent 上下文被无关信息污染。
- sub-agent 的输入输出都是文本,能够被主 Agent 纳入正常消息流。
设计多 Agent 上下文时,可以遵循一个原则:让 sub-agent 输出“结论 + 关键证据”,而不是“完整过程”。这样既保留了可追溯性,又控制了上下文的体量。
5.3 上下文隔离、共享与安全
多 Agent 协作中最容易踩的坑是上下文串味。比如两个子任务分别查询“华东销售数据”和“华北库存数据”,如果共享同一个上下文,模型很可能把两个区域的数据搞混。
工程上通常做三层隔离:
- 数据隔离:每个 sub-agent 只允许访问指定数据源。
- 上下文隔离:每个 sub-agent 使用独立的上下文引擎实例。
- 权限隔离:sub-agent 调用的工具、API key、系统指令最小化。
同时要关注 Agent 安全。不要让用户的提示词轻易改写系统约束,不要在 Prompt 中拼接未经过滤的外部数据,不要把敏感信息写入日志或记忆库。
6. 常见问题与排查思路
6.1 高频问题汇总
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 对话越长,Agent 回答越偏 | 旧消息过多,关键信息被稀释 | 引入摘要记忆,压缩旧消息;把核心约束放入 system prompt |
| Agent 重复调用同一个工具 | tool result 没有保存回上下文 | 检查工具调用是否在历史消息中成对保存 |
| Token 消耗快速上涨 | 每轮都发送全部历史 | 使用滑动窗口,限制最大消息条数和 Token 预算 |
| 换新会话后丢失用户偏好 | 没有长期记忆 | 将关键信息写入向量库,构建 Prompt 前检索召回 |
| sub-agent 结果干扰主 Agent 判断 | 上下文没有隔离 | sub-agent 只返回结果摘要,不返回原始过程 |
| 模型忘记用户第一轮指定的约束 | 约束被滑动窗口丢弃 | 提取约束并写入 system prompt 或摘要 |
| Agent 执行超时 | 上下文过大导致请求过慢,或执行提供方响应超时 | 压缩上下文、缩短超时链路、调整执行超时配置 |
6.2 “Agent执行超时”排查思路
有些 Agent 平台在底层执行任务时,会返回类似the agent execution provider did not respond in time. this may indicate the ...的错误。这类错误往往不是某一个环节的问题,而是整条链路延迟累积导致的。
可以按下面的顺序排查:
- 确认模型响应耗时。如果模型本身需要几十秒才返回,说明不是 Agent 框架的问题,而是模型侧延迟。
- 检查请求上下文大小。上下文过大,会让 prefill 阶段耗时变高,从而拉长整体响应时间。
- 检查工具调用是否有慢依赖。比如某个工具内部请求外部服务超时,会导致整个 Agent 循环卡住。
- 检查是否有多余的重试逻辑。Agent 循环中对失败的工具调用盲目重试,会指数级放大延迟。
- 调整超时配置。给不同执行环节设置合理的超时时间,而不是统一用一个较大的值。
7. 上下文引擎的最佳实践与工程建议
7.1 上下文分层管理
在实现上下文引擎时,建议把“不可变信息”和“可变信息”分开。
- 不可变信息:系统提示词、用户身份、全局规则。
- 摘要信息:历史对话压缩后的结果。
- 短期信息:最近几轮对话。
- 检索信息:当前查询召回的相关记忆。
- 动态状态:当前任务目标、正在执行的工具调用。
每层信息在 Prompt 中的位置不同,更新频率也不同。建议在代码层面使用独立的字段维护,而不是塞进一个无序列表。
7.2 记忆写入要克制
长期记忆不是越全越好。把每句话都写入记忆库,会导致检索时召回大量无关内容,反而降低回答质量。
推荐在写入前做一次判断:
- 是否包含可复用的用户偏好?
- 是否包含已确认的事实?
- 是否是任务的关键结论?
- 是否涉及敏感信息,是否需要脱敏?
不符合条件的,直接丢弃。很多生产级 Agent 会引入一条独立的“记忆抽取 Prompt”,在每轮对话结束后判断并抽取值得长期保存的信息。
7.3 Token 预算管理
除了消息条数限制,还应该监控整体的 Token 使用。建议记录:
- 每个会话的历史 Token 峰值。
- 摘要生成消耗的额外 Token。
- 工具调用参数和结果的平均 Token。
通过日志观察数据后,再动态调整滑动窗口大小、摘要触发阈值、记忆召回数量。不同业务场景的 Token 预算差异很大,没有统一答案。
7.4 可观测性
上下文引擎是状态密集型组件,必须有日志和快照能力。每次构建 Prompt 前,可以输出:
- 当前短期消息条数。
- 摘要是否触发。
- 记忆检索命中了哪些条目。
- 最终发送的消息总 Token 数。
同时,把每次请求的完整上下文做一次快照。当 Agent 回答异常时,可以回放当时的上下文,快速定位是“信息缺失”还是“模型决策问题”。
7.5 与 Skill 和 MCP 的关系
Agent 开发中,经常听到 Skill 和 Agent Skill 的概念。Skill 可以理解为一个 Agent 可复用的特定能力,比如“生成周报”“SQL 查数”。MCP 则是一种工具接入的标准化协议,用于让模型、Agent 和外部工具之间按统一格式交换上下文。
简单理解:
- Skill 偏重量级应用层能力,回答“Agent 能做什么”。
- MCP 偏重量级协议层接入,回答“工具怎么被 Agent 调用”。
无论采用哪种方式,它们产出的结果最终都要回到上下文引擎统一管理。工具返回的数据、技能执行的中间状态,都应该成为上下文的一部分,而不是游离在消息列表之外。
8. 接下来可以怎么练
上下文引擎没有银弹,不同业务场景的策略差异很大。建议从三个方向继续深入:
第一,把本文的ContextEngine代码跑通,在真实的多轮对话中加入工具调用,观察滑动窗口和摘要触发是否合理。第二,接入真实向量库,把关键词检索替换成 Embedding 向量检索,对比召回效果。第三,做一个简单的多 Agent 主从模式,让 sub-agent 执行子任务并返回摘要,体会上下文隔离的价值。
如果能长期坚持记录上下文大小、Token 消耗、回答质量和召回命中率,你会逐渐形成自己的上下文治理方法论。这也是从“会搭 Agent”到“能做生产级 AI Engineer”的重要分水岭。