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类里,没有分类,也没有命名约束,光是把不同类型的信息分离出来就花了两天时间,期间还不小心破坏了原有逻辑,导致线上出现了一系列问答异常。从那以后,我坚持在项目一开始就定义
