1. 项目背景:为什么突然要聊Context
这两年做大模型应用,绕不开的一个词就是Context。从开发者的角度去看,它在不同场景里代表的东西其实不一样:可能是输入给模型的上下文窗口,可能是RAG流程里拼进提示词的那一批检索片段,也可能是Agent运行过程中维护的一段会话状态。标题里这个"Blog #195: Context体系与类型",本质上就是在梳理这团概念——到底Context有哪些类型,它们之间是什么关系,一个完整项目里应该如何分层管理。
我最早接触Context这个概念,是在做长文本问答的时候。当时的场景很简单:用户上传一份几十页的PDF,希望模型能够根据文档内容回答细节问题。而模型本身的上下文窗口有限,不可能把整份文档都塞进去。于是就必须考虑:哪些内容应该放进Context,怎么组织这些内容,才能让模型在有限的窗口内做出尽可能准确的回答。后来做了越来越多的Agent类项目,发现Context的管理难度又上了一个台阶——不仅涉及单次请求的上下文,还涉及跨轮对话的累计上下文、系统指令、工具调用记录、临时变量等等,它们全部属于Context体系的范畴。
这篇文章就把我在实际项目中梳理出来的Context体系做一个系统性的拆解。适合谁看?一是正在做RAG或Agent应用开发,但总觉得上下文拼装混乱、效果不稳定的工程师;二是想从"调用API"走向"设计一个完整LLM应用"的同学。内容上不会停留在概念层面,而是会把核心类型、分层设计、实现细节、坑点都铺开讲清楚,尽量做到能直接落地参考。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Context的完整体系与类型拆解
2.1 从一次API调用看Context的组成
先从一个最简单的视角切入:当你向一个大模型API发起一次请求时,实际发送给模型的内容,就是一次性的全部Context。我习惯把这一份Context拆成四类:系统上下文、用户上下文、工具/函数上下文、历史对话上下文。
系统上下文通常对应System Prompt,它告诉模型"你扮演什么角色、输出什么格式、遵守什么规则"。用户上下文是用户当前这一次的输入,可能是一条问题、一段代码、一个指令。工具上下文在函数调用场景中比较特殊,它包含了可用工具的描述、工具返回的结果、本次调用链路的中间输出。历史对话上下文则是之前多轮对话的累计会话记录。
这四个部分组合在一起,构成了一次请求的完整"输入空间"。模型的所有输出,都是基于这四类上下文综合推理得出的。理解这一点很关键,因为很多效果问题,归根结底是"某一类上下文没设计好",而不是模型本身不行。
举个例子。用户问:"上个月的销售数据帮我分析一下。"如果系统上下文里有清晰的角色设定和输出规范,历史上下文里有之前约定好的数据分析模板,工具上下文里已经拉取到了上个月的销售明细表,那么模型就能给出靠谱的分析。但如果其中任意一环缺失——比如历史上下文被截断、工具结果没有正确拼入,模型就会开始"自由发挥",结果就不可控了。
2.2 按生命周期划分:静态、动态与临时Context
另一套划分方式是从生命周期的维度去看,这样更方便做工程实现。
静态Context是几乎不变化的那些内容。典型的就是系统提示词、固定的知识库索引说明、用户填写的企业资料、产品功能清单。一般会在应用启动时加载,或者随请求每次都带上(视长度而定)。静态Context的特点是稳定、可复用、适合做缓存。
动态Context则是每次请求都可能不同的内容。比如基于用户这一次输入检索到的RAG片段、实时拉取到的订单数据、上一步工具调用的返回结果。这些内容必须在请求发起前实时组装,无法提前准备。
临时Context更像是"运行时状态"。在Agent多步推理的场景中,每一步产生的新信息——比如模型刚刚生成的工具调用参数、执行结果、中间结论——都需要暂存下来,作为下一步推理的输入。这部分Context的特点是生命周期极短,只存在于当前这个任务链路内,任务结束后即可丢弃。
按生命周期划分的意义在于指导存储策略。静态Context适合放缓存或持久化存储,动态Context适合走实时计算链路,临时Context则交给内存或短生命周期存储来管理。很多项目出问题,就是因为没有区分这三类,一股脑全部塞进同一个存储结构里,最终导致管理混乱、性能下降。
2.3 按业务属性划分:语义Context、时序Context与状态Context
如果从业务语义的角度去拆,又会有另一种分类,而且这套分类在Agent场景中格外有用。
语义Context是"文档说了什么"这一层的信息,通常来源于知识库检索、文档片段、结构化数据表述。它帮助模型理解事实性内容,是RAG系统的核心。时序Context是"事情发生的先后顺序"这一层的信息,在多轮对话、事件追踪、日志分析中尤为重要。比如用户先说要买A产品,又说预算不够要换B产品,这里的时间先后、状态流转,就属于时序Context。状态Context则是"当前系统处于什么状态"的信息,包括用户选择了什么、任务进行到哪一步、哪些条件已满足、哪些条件还差。
这三类Context的用途截然不同。语义Context决定模型的"知识广度",时序Context决定模型的"推理连贯性",状态Context决定模型的"决策准确性"。在设计一个复杂Agent时,我会刻意把这三类信息在内部做标注区分。比如,状态Context用结构化的JSON存,时序Context保持在对话流中不丢顺序,语义Context则来自检索系统,并标注来源与置信度。
注意:很多应用只重视语义Context,把知识片段尽量多地塞给模型,却忽略了时序Context和状态Context。结果就是模型看起来很博学,但做出的决策却前后矛盾,或者反复执行同一个步骤而不自知。
2.4 Context的粒度:Token、消息还是文档
Context在实际编码中还涉及粒度问题。我们谈论上下文长度的时候,单位通常是Token;但在应用层设计时,操作的单位往往是消息或文档块。
Token是最底层的粒度,它对应模型的计费与窗口限制。每条不同的模型,上下文窗口的大小就是Token数量上限。消息粒度在对话类应用中更常用:数组里的每条Message对象有自己的role和content,这类结构适合表达多轮会话。文档粒度则是面向RAG和知识库场景的:把一篇长文档切分成多个chunk,每个chunk附带元信息(来源、标题、页码等),拼入Context时以chunk为单位做筛选和排序。
我见过不少项目把三个粒度混为一谈。比如,在一些代码里,开发者直接用字符串拼接的方式来组装Context,结果不仅token数量失控,连消息的role信息都丢失了——模型分不清哪句是用户说的、哪句是系统说的、哪句是工具返回的,推理质量迅速劣化。正确的做法应该是在代码层面用结构化的消息对象来承载Context,再在序列化阶段统一转换成模型要求的格式。
2.5 Context的类型边界与命名规范
实际协作中,Context的分类还需要落到一个可执行的规范上,否则不同开发者在代码里对"上下文"的理解会产生分歧。
我给团队常用的约定是:在代码中把Context相关的类名和变量名统一加上前缀,比如SysContext、UserContext、ToolContext、HistoryContext。每个Context类内部负责自己的构建逻辑,不跨层互相修改。比如UserContext只负责处理用户当前输入,不负责检索文档;ToolContext只负责管理工具调用链路的输入输出,不负责拼接历史消息。这样即使多人协作,每个人也能很快定位到某一类Context的逻辑位置。
命名规范听起来简单,但在项目膨胀之后价值极大。有一次我接手一个老项目,里面的上下文拼接散落在一个几千行的Service类里,没有分类,也没有命名约束,光是把不同类型的信息分离出来就花了两天时间,期间还不小心破坏了原有逻辑,导致线上出现了一系列问答异常。从那以后,我坚持在项目一开始就定义Context的类型边界。
3. 核心细节解析与实操要点
3.1 系统Context的设计:越明确越好
系统Context(System Prompt)是最容易忽视、又最影响效果的一部分。很多初学者的做法是随便写一句"你是一个AI助手",然后就没了。这相当于把一个没有任何工作准则的人拉进会议室,他只能自由发挥。
在我的项目里,系统Context会包含至少五类信息:角色定义、任务目标、输出格式规范、行为约束、兜底策略。角色定义说明"你是谁",任务目标说明"你要完成什么",输出格式规范说明"按什么结构输出",行为约束说明"什么不能做",兜底策略说明"信息不足时怎么办"。
举个例子,一个客服场景的系统Context可以这样写:
- 角色:你是某电商平台的客服助手。
- 任务:根据用户问题,结合知识库内容给出准确回复。
- 输出:回复必须控制在100字以内,结尾加上"还有其他需要帮助的吗?"
- 约束:只回答与订单、退换货、物流相关的问题,其他问题引导用户联系人工客服。
- 兜底:如果知识库中没有对应信息,明确告知用户"该问题我需要转接人工处理"。
这五类信息看起来简单,但每一条都会实质性地影响模型行为。输出格式规范尤其重要——没有它,模型就可能在回答里加入大量与任务无关的内容,增大Token消耗,也让下游解析变得困难。
3.2 用户Context的清洗与结构化
用户输入是最灵活、最不可控的部分,所以对用户Context做清洗和结构化是必做动作。
首先要做长度控制。用户可能一次性粘贴几千字的长文,也可能只输入一个词。系统需要对输入长度做合理限制,或者在超长时引导用户精简。其次是格式归一化,用户输入的换行、多余空格、特殊符号,可能在交给模型前需要做规范化处理。再者是敏感信息脱敏,涉及身份证号、手机号、地址等内容,需要在进入Context前做好匿名化处理,避免隐私数据随请求发送给模型。
结构化方面,建议对用户输入做意图识别和实体抽取后,再构造一个干净的UserContext。例如用户说"我要退掉昨天买的那双鞋",可以结构化提取意图为"退货",实体为"昨天买的鞋"。这样模型后续在处理时,不用反复从原始文本里解析,推理效率和准确率都会提升。
实操心得:用户原始输入永远不要直接丢给模型。先经过清洗、截断、结构化三步,再进入Context体系。哪怕只是简单的规则清洗,也能明显改善模型在复杂输入下的表现。
3.3 工具Context的组织:一次调用的来龙去脉
在Function Calling或Agent场景里,工具Context的复杂性远高于系统Context和用户Context。
工具Context需要承载的信息至少包括三类:可用的工具清单、本次调用的入参、工具执行后的返回结果。工具清单一般以JSON Schema的形式传给模型,告知模型有哪些函数可以调用、每个函数的参数结构是什么。本次调用的入参是模型基于当前对话生成的,工具执行系统拿到入参后去执行真实逻辑。工具执行后的返回结果,需要再拼回Context中,供模型进行下一步推理。
这里有一个非常容易踩坑的细节:工具返回结果可能很长。比如一个数据库查询工具返回了100行数据,全部塞进Context,Token消耗巨大,而且其中可能只有少数几行对当前任务有用。我的做法是,在工具执行后加入一个精简层——对返回结果做摘要、截断、字段筛选,只保留与当前任务最相关的信息再进入Context。
另外,工具调用的过程记录也要进入Context,但记录方式要有讲究。建议按照"思考->调用->观察->结论"的格式来组织,让模型能够沿着链路理解为什么调用这个工具、结果是什么、接下来怎么办。这种结构化的链路记录,比单纯把工具结果堆在一起要有效得多。
3.4 历史Context的管理:窗口、压缩与摘要
历史对话是Context管理中工程复杂度最高的部分。
先讲窗口策略。最简单的是滑动窗口:只保留最近N条对话记录,更早的直接丢弃。这种策略省事,但会丢失早期对话中的关键信息。稍微复杂一点的是Token数量控制:按累计Token数来截断,保证不超过模型窗口上限。更高级的做法是摘要压缩:每隔几轮对话,用模型对之前的对话生成一段摘要,存为压缩后的历史Context,在后续请求中同时携带摘要和最近几条完整对话。
我在项目里测试过三种策略的效果,滑动窗口最适合对早期信息依赖度不高的场景(如单轮问答)。Token数量控制在长对话场景中可以确保请求不超限,但依然会造成信息断裂。摘要压缩的效果最好,但会增加额外的模型调用开销。
摘要压缩有点像一个会议记录员,每隔一段时间就总结一次"刚才都聊了什么",然后在后续讨论中带着这份总结继续,同时保留最近几轮的完整记录。这个"保留最近几轮"很重要,因为摘要会丢失细节,而最近几轮往往包含了用户最新的意图和偏好,必须完整保留。
3.5 Context窗口溢出:从报错信息定位问题
现在业界很多模型上下文窗口已经做得很大,比如有模型宣称支持百万Token级别的上下文窗口。但实际开发中,窗口溢出仍然是高频问题,尤其是Agent场景。热词里那些报错信息,比如"context length exceeded (169,692 tokens)"、"codex ran out of room in the model's context window",本质上就是Context管理不到位导致的。
碰到Context窗口溢出,我的第一反应不是改一行代码,而是先看看到底是哪一类Context膨胀了。通常原因有三类:历史对话累积太长、工具返回结果太大、RAG检索片段拼入过多。排查方法是把一次请求的Context分环节打日志,统计每一类的Token占比。定位到膨胀源之后,再针对性地做压缩或截断。
比如历史对话太长,就加大摘要压缩频率;工具返回太大,就在工具层加精简逻辑;RAG片段过多,就收紧检索的TopK或做片段重排序,只保留最相关的几个片段进入Context。这个排查思路屡试不爽,远比盲目调大窗口参数靠谱。
3.6 不同模型的Context上限差异与适配
不同模型平台在上下文处理上还存在明显的差异,这对Context体系设计有直接影响。
有些模型在Context超过窗口上限时会直接报错,要求开发者自行处理;有些模型则会在达到上限时自动进行内部压缩,但压缩后的效果往往不可控。就拿热词里那句话来说,"this model's maximum context length is 1048576 tokens",说明这个模型窗口设计得很宽,但宽度并不等于质量。窗口越大,模型对早期信息的注意力越容易被稀释,实际效果不一定比"精炼但聚焦"的小窗口好。
此外,不同模型的计费规则不同,有的按输入输出总Token计费,有的按单独Token类型计费。Context越长,成本越高,这是逃不掉的。所以在设计Context体系时,我会先确定模型选型,再根据这个模型的具体窗口上限、压缩策略、计费模式来设计Context的长度预算和压缩策略。不同模型之间不能照搬同一套Context方案。
注意:模型窗口大不等于可以随便塞。上下文越长,单次请求延迟越高、成本越高、注意力分散越严重。合理的目标不是"塞满窗口",而是"用最少的关键信息覆盖最重要的推理维度"。
4. 实操过程与核心环节实现
4.1 设计一个简化版Context管理器
这一节,我以一个实际的简化版Context管理器为例,展示代码层面的实现思路。这个管理器负责组装一次请求的完整Context,支持四类Context组合,并控制总Token数。
先定义数据结构:
python复制from dataclasses import dataclass, field
from typing import List, Dict, Optional
@dataclass
class SystemContext:
content: str
token_budget: int = 500
@dataclass
class UserContext:
content: str
structured_data: Optional[Dict] = None
@dataclass
class ToolContext:
tool_name: str
arguments: Dict
result: Optional[str] = None
truncated: bool = False
@dataclass
class HistoryContext:
messages: List[Dict] = field(default_factory=list)
summary: Optional[str] = None
这个结构对应了前文说的四类Context。每个对象内部持有自己的内容,便于后续做分类统计和压缩。
然后组装完整上下文,并做Token估算和截断:
python复制class ContextManager:
def __init__(self, model_max_tokens: int = 8000, response_reserved: int = 1000):
self.model_max_tokens = model_max_tokens
self.response_reserved = response_reserved
def estimate_tokens(self, text: str) -> int:
# 简易估算:英文约4字符/token,中文约1.5字符/token
# 实际项目可以使用tokenizer库精确计算
return max(1, int(len(text) / 2.5))
def build_context(
self,
system_ctx: SystemContext,
user_ctx: UserContext,
tool_ctxs: List[ToolContext],
history_ctx: HistoryContext
) -> List[Dict]:
messages = []
if system_ctx:
messages.append({"role": "system", "content": system_ctx.content})
# 历史上下文:摘要+最近部分
if history_ctx.summary:
messages.append({"role": "system", "content": f"[对话摘要] {history_ctx.summary}"})
for msg in history_ctx.messages:
messages.append(msg)
# 当前用户输入
messages.append({"role": "user", "content": user_ctx.content})
# 工具上下文:以tool结果按顺序拼入
for tool_ctx in tool_ctxs:
messages.append({
"role": "assistant",
"content": None,
"tool_calls": [{
"id": tool_ctx.tool_name,
"type": "function",
"function": {
"name": tool_ctx.tool_name,
"arguments": str(tool_ctx.arguments)
}
}]
})
messages.append({
"role": "tool",
"tool_call_id": tool_ctx.tool_name,
"content": tool_ctx.result or ""
})
# 总Token控制
total_tokens = sum(self.estimate_tokens(m.get("content") or "") for m in messages)
available_tokens = self.model_max_tokens - self.response_reserved
if total_tokens > available_tokens:
messages = self._truncate_context(messages, available_tokens)
return messages
def _truncate_context(self, messages: List[Dict], budget: int) -> List[Dict]:
# 保留系统消息,截断最旧的历史消息
system_msgs = [m for m in messages if m["role"] == "system"]
non_system_msgs = [m for m in messages if m["role"] != "system"]
truncated = []
used = sum(self.estimate_tokens(m.get("content") or "") for m in system_msgs)
# 从最新消息向前保留,直到预算耗尽
for m in reversed(non_system_msgs):
m_tokens = self.estimate_tokens(m.get("content") or "")
if used + m_tokens > budget:
break
truncated.insert(0, m)
used += m_tokens
return system_msgs + truncated
这段代码的核心思路是:先把各类Context按顺序拼装成标准消息数组,最后做一次整体Token预算检查,超出预算时从最旧的非系统消息开始截断,保证系统指令和最近的用户输入不被丢弃。
4.2 历史消息压缩的落地实现
上面示例中还没涉及摘要压缩的生成逻辑,我单独补充一段。当历史消息达到一定长度时,触发一次摘要生成,用之前的对话记录喂给模型生成压缩摘要:
python复制def summarize_history(history_messages: List[Dict], summarize_llm) -> str:
# 将历史消息转换为文本输入
history_text = "\n".join(
f"{m['role']}: {m.get('content') or ''}" for m in history_messages
)
prompt = f"""请将以下对话压缩为一个简明摘要,保留关键信息:
- 用户的核心需求
- 已经确认的事实
- 尚未解决的问题
- 用户的偏好和语气
对话内容:
{history_text}
"""
result = summarize_llm.chat([{"role": "user", "content": prompt}])
return result
摘要生成这个动作本身也是一次LLM调用,所以不应该每轮都触发。我的触发条件是:历史消息总Token数超过预设阈值的两倍时,才执行一次摘要生成,生成后把旧的对话替换为摘要,外加最近N条完整消息。
4.3 RAG场景下的Context组装实战
RAG(检索增强生成)场景是Context体系应用最广泛的领域之一,它的核心问题在于:检索回来的一堆文档片段,怎么选、怎么排序、怎么拼,才能让模型输出质量最高。
我的推荐流程是:先召回(Retrieve),再重排(Rerank),最后精选进Context。召回阶段,用向量检索或BM25拿回TopK个候选片段,K可以设置得宽松一些,比如20到50。重排阶段,用重排模型对候选片段与用户问题的相关度做精细打分,取TopN(比如3到5)个最终进入Context。精选阶段,对最终选中的片段做截断和摘要,只保留与问题最相关的段落。
RAG拼装的一个关键是保持文档来源信息。每个进入Context的片段,都应该带上来源标签,比如"来自《产品手册》第3章第2节"。这样模型在回答时可以引用来源,也便于下游做引用追溯。
代码级实现:
python复制def build_rag_context(question: str, retrieved_chunks: List[Dict], top_n: int = 3) -> str:
# 这里的retrieved_chunks每项包含chunk_text、score、source、page等字段
# 1. 重排(简化示例:按已有相似度分数取top_n,真实场景会调用rerank模型)
ranked_chunks = sorted(retrieved_chunks, key=lambda x: x["score"], reverse=True)[:top_n]
# 2. 组装带来源的片段
parts = []
for idx, chunk in enumerate(ranked_chunks):
source_info = f"来源:{chunk['source']}"
if chunk.get("page"):
source_info += f",第{chunk['page']}页"
parts.append(f"[{idx + 1}] ({source_info})\n{chunk['chunk_text']}")
# 3. 将参考内容拼入用户上下文
context_text = "\n\n".join(parts)
augmented_user_input = f"""请基于以下参考内容回答问题。
参考内容:
{context_text}
用户问题:
{question}
要求:
- 优先使用参考内容作答
- 如果参考内容不足以回答,明确说明信息不足
- 引用内容时标注来源编号
"""
return augmented_user_input
这段实现的核心是两层:第一层,用重排缩小进入Context的片段数量,保证信息精度;第二层,用明确的指令模板告诉模型"如何对待这些参考内容"。很多RAG项目效果不稳定,问题就出在"重排缺失"和"指令不清"这两点上。
4.4 Agent多轮规划中的状态Context管理
Agent场景下,模型通常需要进行多轮推理和工具调用,状态Context的维护是整个系统能否正确收敛的关键。
我习惯用一个SessionState对象来存储Agent运行中的状态信息。这个对象包含:当前任务描述、已完成步骤列表、下一步计划、已收集的关键信息、需要避开的错误方向。在每次调用模型前,把SessionState序列化为一段文本拼入Context;在每次工具返回后,更新SessionState。
python复制@dataclass
class SessionState:
task: str
completed_steps: List[str] = field(default_factory=list)
next_plan: List[str] = field(default_factory=list)
key_facts: List[str] = field(default_factory=list)
errors_to_avoid: List[str] = field(default_factory=list)
def to_context_text(self) -> str:
lines = [f"当前任务:{self.task}"]
if self.completed_steps:
lines.append("已完成步骤:" + "; ".join(self.completed_steps))
if self.next_plan:
lines.append("后续计划:" + "; ".join(self.next_plan))
if self.key_facts:
lines.append("关键事实:" + "; ".join(self.key_facts))
if self.errors_to_avoid:
lines.append("需避免的错误:" + "; ".join(self.errors_to_avoid))
return "\n".join(lines)
在Agent循环中,每一步执行完毕都会更新这个SessionState。这样,模型在下一步推理时,不仅能看到最新的工具输出,还能看到全局状态,不会陷入"走一步忘一步"的循环里。热词里提到的"codex ran out of room in the model's context window. start a new thread"这类问题,很多时候就是因为状态Context没有管理好,导致每一轮都把全部历史重新拼进去,窗口迅速耗尽。
实操心得:Agent场景中,上下文管理的目标不是"最大化保留信息",而是"在每一步保留做决策所需的最小充分信息"。一个设计良好的SessionState,加上被摘要压缩的历史,加上精简后的工具输出,三者组合起来,通常就足够支撑复杂任务了。
5. 常见问题与排查技巧实录
5.1 上下文越拼越长,最终触发窗口限制
这是最普遍的问题。现象是运行一段时间后,报错信息里出现类似"context length exceeded"或"this model's maximum context length..."的提示。
我的排查步骤是这样的:第一,确认报错发生在哪一环,是请求前拼装超限还是模型返回后才超限;第二,查看请求日志,把组成Context的各环节Token数打出来,定位膨胀源;第三,针对膨胀源做治理。
治理手段按优先级排列:先压缩工具返回结果,这个往往是最无意识的膨胀源;再压缩历史消息,加大摘要频率;然后控制RAG片段数量;最后再考虑提升模型窗口或换更贵的模型。不要一上来就换大窗口模型,那等于用钱掩盖设计缺陷。
比如有一次,一个Agent在处理一个数据分析任务时反复报上下文超限。排查后发现,每次工具调用都返回了完整的数据库表结构,而表结构有几百个字段,占了大量Token。处理方式是只返回当前任务涉及的字段列表,Token消耗直接降低了一个数量级,问题立刻解决。
5.2 模型"健忘",过了几轮对话就不记得前面说了什么
这类问题的根源通常在于历史Context被过度截断或摘要压缩丢失了关键信息。模型不是真的"健忘",而是你根本没把信息给它。
对策有三步:第一,确认关键信息是否进入了摘要——比如用户名、偏好、已确认的事实,这些必须在摘要里显式保留;第二,增加"关键信息固定槽位",把必须长期记住的内容(如用户ID、任务目标)放在系统Context或单独的固定字段中,不随历史消息一起压缩;第三,适当放宽滑动窗口大小,保留更多最近几轮的完整内容。
提示:如果用户明确说过"记住我叫小王",那么"小王"这个信息就不应该依赖历史消息保存,而应该被抽取出来,放进一个持久化的用户画像Context里,每次请求都带上。
5.3 工具调用链路一长,模型就开始"乱来"
Agent在连续调用多个工具时,经常出现模型突然不按计划执行、重复调用同一个工具、或者编造工具结果的情况。
这通常与工具Context的丢失或混乱有关。一个常见原因是工具调用历史在Context中被截断了,模型只看到了当前这一步的工具返回,却看不到前面几步的调用过程,于是无法理解自己身处推理链的哪一环。另一个原因是工具返回结果格式不统一,模型难以从混乱的信息中提取有效内容。
解决方案:确保工具调用链路以结构化文本形式完整保留在Context中,不被普通历史截断策略误伤;工具返回结果统一采用"调用工具名+参数摘要+结果摘要"三段式;在状态Context中加入"当前进度"字段,让模型随时知道已经做到了哪一步。
5.4 不同请求之间上下文串扰
有些应用在同一个后端进程里处理多个用户的请求,如果Context存储没有按会话隔离,就会出现A用户的问题跑到B用户的Context里。这类问题往往很隐蔽,因为不是必现,只在多用户并发时偶发。
解决办法很简单:所有Context对象都必须绑定session_id,所有读写操作都按session_id进行隔离。另外,建议每次请求完成后显式释放临时Context,避免内存滞留和跨请求污染。在测试阶段,可以专门写一个并发脚本,模拟多用户同时访问,验证Context是否真正隔离。
5.5 Context过长的SQL或文档内容处理经验
在代码生成或文档分析场景中,输入内容可能是一整个项目目录下的多个文件,或者一段几千行的SQL。盲目全部塞入Context,既浪费Token,又可能导致模型抓不住重点。
我的做法是分两层处理。第一层,先用代码结构化工具对输入内容做索引,提取文件清单、函数清单、表结构清单;第二层,根据用户问题,先做一次"预检索",只选取与问题相关的函数、表或代码块加入Context。如果用户的问题涉及全局,比如"把整个项目的架构分析一下",那就必须使用分步策略:先让模型分析每个模块,再汇总成整体结论,而不是一次性把所有代码都塞进去。
这里还有一个小技巧:同样是长文本,直接贴原文的效果,通常不如"原文摘要+关键片段原文"的组合。摘要帮模型建立全局理解,片段原文则提供精确细节,两者配合比单一策略更好用。
6. Context体系的进阶思路
6.1 Context工程与代理技能进化的关系
热词里提到一个概念叫"meta context engineering via agentic skill evolution",虽然表述比较新,但方向很明确:Context体系不是一成不变的,它可以根据Agent的自我进化而动态调整。
通俗一点说,一个Agent在使用过程中,可能会发现某种Context组织方式特别有效,于是把这个经验沉淀下来,形成一种"技能"。下次遇到同类任务时,Agent可以直接复用这套Context组织模式,而不必从头开始构建。这相当于给Context体系加一层"经验层",让它在实践中不断优化。
目前这个概念还处于前沿探索阶段,实现方式也大多是实验性的。我的建议是,先把基础的分层Context体系做扎实,再考虑引入自动化调优。基础都没打牢就追新概念,容易变成空中楼阁。
6.2 Context可视化与调试工具的重要性
Context体系做得越复杂,调试就越困难。很多时候模型输出不对,你很难直观地看出是哪一类Context出了偏差。所以,给Context体系配套可视化和调试能力,是非常值得的投资。
我常用的调试手段包括:在每次请求的日志里输出Context摘要(包含各类Context的Token占比、关键字段、截断情况)、把完整Context保存到本地供事后回放、提供一个内部调试页面可以查看指定会话的Context快照。这些工具虽然要花一些开发时间,但对排查问题的效率提升是指数级的。
Graphify这类知识图谱工具也与Context体系有结合点。可以把多轮对话中提取出的实体和关系写入图结构,在构建Context时先查询图谱,获取与当前问题相关的实体链路,作为语义Context的一部分。这种方式在复杂知识问答中表现很好,也是我下一个准备深入的方向。
6.3 成本与性能的平衡取舍
最后还是要回到成本和性能的平衡问题上。Context越长,单次请求的延迟和费用就越高。在商业应用中,这直接决定了产品的毛利和用户体验。
我的原则是:默认走最小充分Context策略,每次请求只携带做决策所需的信息。只有在模型明确反馈信息不足时,才逐级扩展Context范围。这听起来简单,但落地需要细致的埋点和分析。比如,统计不同类型请求的平均Context Token数、平均重试次数、模型回答质量评分,根据这些数据持续调整Context策略。不要迷信"长上下文模型可以解决一切",好的Context设计,可以在很小的窗口内解决绝大多数问题。
7. 踩坑后的几点个人经验
写到最后,分享几个我做Context体系至今印象最深的感触。
第一,Context不是越全越好。很多产品喜欢一股脑把所有背景信息都塞给模型,担心给少了模型答不好。事实恰恰相反,过多的冗余信息反而会稀释模型对关键内容的注意力。我见过一个知识库问答项目,从检索TopK从5调到20之后,回答准确率不升反降,就是因为混入了太多低相关片段。Context的核心是"精",不是"多"。
第二,结构化的价值被严重低估。同样一堆信息,用纯文本拼进去,和用结构化的消息对象、状态字段组织后拼进去,模型的理解效果差距极大。我后来做的项目里,几乎每个进入Context的信息块都有明确角色和边界,模型很少再出现张冠李戴式的错误。
第三,Context管理要趁早。项目初期觉得"直接拼字符串就够了",等到对话轮次多了、工具调用多了、用户量大了,再回头重构Context体系,成本会成倍增加。架构设计的前瞻性,在这里体现得非常明显。
第四,一切以可观测为准。Context体系不能是黑盒子,每一类Context的构成、Token消耗、截断情况都要有日志和监控。没有可观测性,出了问题你只能靠猜,而靠猜调Context,是最浪费时间的事情。
如果你正在做RAG、Agent或任何涉及大模型对话的应用,建议花一个下午的时间,把自己现有的Context拼装逻辑画出来,按照本文的框架分类,看看哪些类型缺失了,哪些类型冗余了。把这一步做扎实,后面很多效果问题都会迎刃而解。
