Skip to content

3.3 上下文工程:怎么管理一个会话里的一切

前两节讲了记忆的演进和管理。这一节讲更大的问题:一个会话里,所有信息怎么组织、怎么注入到 LLM 的上下文中?

一个会话里有多少信息?

一个会话里,LLM 需要知道的东西有很多:

  • 系统提示词(你是谁、能做什么)
  • 记忆事实(用户叫什么、项目用什么技术栈)
  • 项目上下文(AGENTS.md、RepoWiki)
  • Skill 文档(怎么用某个工具)
  • 监控数据(这次调用花了多少 Token)
  • 对话历史(前面聊了什么)

如果手动拼接,每加一种来源就要改代码:

csharp
// 旧做法:手动拼接,改一处动全身
var messages = new List<ChatMessage>();
messages.Add(new ChatMessage(ChatRole.System, systemPrompt));
messages.Add(new ChatMessage(ChatRole.System, memoryFacts));
messages.Add(new ChatMessage(ChatRole.System, projectContext));
messages.Add(new ChatMessage(ChatRole.System, skillDocs));
// ... 还有更多
messages.AddRange(conversationHistory);
messages.Add(new ChatMessage(ChatRole.User, userInput));

这种做法的问题是:耦合太紧,扩展太难。

加记忆要改,加监控要改,加 Skill 也要改。每加一种上下文来源,就要动这段代码。

上下文提供者链

上下文提供者链:可组合的信息注入架构

更好的做法:上下文提供者链(AIContextProvider Chain)

每种上下文来源都是一个独立的 Provider,框架自动收集、依次执行:

csharp
// 注册上下文提供者
services.AddSingleton<AIContextProvider>(sp => 
    sp.GetRequiredService<MemoryFactsContextProvider>());   // 记忆事实
services.AddSingleton<AIContextProvider>(sp => 
    sp.GetRequiredService<MonitoringContextProvider>());    // 监控数据
services.AddSingleton<AIContextProvider>(sp => 
    sp.GetRequiredService<TitleContextProvider>());         // 会话标题
services.AddSingleton<AIContextProvider>(sp =>
{
    var manager = sp.GetRequiredService<AgentSkillsManager>();
    return manager.BuildProvider();                          // Skill 文档
});

每个 Provider 只做一件事:

csharp
internal class MemoryFactsContextProvider : AIContextProvider
{
    protected override async ValueTask<AIContext> ProvideAIContextAsync(
        AIContextProvider.InvokingContext context,
        CancellationToken cancellationToken)
    {
        var sessionId = GetSessionId(context.Session);
        var facts = await _memoryStore.LoadFactsAsync(sessionId);
        
        if (facts.Count == 0) return new AIContext();
        
        var formatted = MemoryFormatter.FormatForPrompt(facts, _options.MaxMemoryTokens);
        
        return new AIContext
        {
            Messages = [new ChatMessage(ChatRole.System, formatted)]
        };
    }
}

执行时机

上下文提供者的执行时机是这样的:

用户发消息

Agent 准备调用 LLM

框架自动执行所有 AIContextProvider:
  ├── MemoryFactsContextProvider  → 加载记忆事实 → 注入 SystemMessage
  ├── MonitoringContextProvider   → 记录调用开始 → 发布事件
  ├── TitleContextProvider        → 更新会话标题
  └── AgentSkillsProvider         → 加载 Skill 文档 → 注入 SystemMessage

所有 Provider 的输出合并到 LLM 上下文

调用 LLM

MonitoringContextProvider 记录调用结束(成功/失败)

这个设计的好处是开闭原则

  • 对扩展开放:新增上下文来源,只需新增一个 Provider
  • 对修改关闭:不需要改 Agent 调用逻辑,不需要改其他 Provider

比如以后要加一个"用户权限上下文",只需要新增一个 UserPermissionContextProvider,注册到 DI 容器,完事。现有的代码一行不改。

检索策略:用结构换检索

除了记忆系统内部的检索,还有一个更大的问题:Agent 怎么找到项目级别的知识?

这里有三种主流策略:

三种检索策略对比:RAG+向量数据库 vs 结构化知识摘要 vs 手工策展上下文

策略原理优点缺点
RAG + 向量数据库文档切片 → Embedding → 相似度搜索自动化程度高需要向量数据库;切片可能破坏结构
结构化知识摘要预生成代码库的结构化摘要精准、省 Token、可调试需要生成和维护
手工策展上下文人工编写项目上下文文件最精准、零基础设施需要人工维护

我选择了第二种和第三种的组合。

为什么不用 RAG?

RAG 的核心假设是:信息分散在大量文档中,需要"搜索"才能找到。

但对于编码场景,这个假设不太成立:

  • 代码有天然的结构——目录、模块、接口,不需要 Embedding 来"发现"关系
  • 开发者知道自己在做什么——不需要"搜索",需要"直接告诉"
  • 向量数据库增加基础设施复杂度,对于本地部署场景是个负担

我选择的方案是 RepoWiki 思路:用结构换检索。

项目根目录/
├── .qoder/
│   └── repowiki/
│       └── zh/
│           ├── overview.md          ← 项目概览
│           ├── modules/
│           │   ├── workflow.md      ← 工作流模块
│           │   ├── aiagent.md       ← Agent 模块
│           │   └── ...
│           └── patterns/
│               ├── architecture.md  ← 架构模式
│               └── conventions.md   ← 编码规范

这份摘要是人类可读的 Markdown,不是黑箱向量。它告诉 Agent:项目有哪些模块、每个模块的职责是什么、关键接口长什么样、编码规范是什么。

Agent 拿到这份摘要,就知道该去哪个模块找代码,不需要"搜索"。

再加上手工策展的 AGENTS.md:

AGENTS.md 是更精炼的一层——它不是"项目全貌",而是"Agent 需要知道的最重要的事"。比如:

markdown
## 后端架构(五层)
API 层 → Application 层 → Domain 层 → Repository 层 → Model 层

## 命名规范
| 层级 | 类型 | 示例 |
|------|------|------|
| API | `{模块名}Controller` | `PluginController` |

RepoWiki 是"地图",AGENTS.md 是"指南针"。一个告诉你全貌,一个告诉你重点。

业界怎么做的

同样的"上下文管理"问题,其他框架的思路各不相同:

Hermes 走的是"记忆提供者"路线。核心抽象是 MemoryProvider,有完整的生命周期:initialize → prefetch → sync_turn → shutdown。支持插件式扩展——内置提供者 + 最多一个外部提供者(Honcho、Mem0 等)。关键设计:每轮对话前主动召回相关记忆,每轮对话后异步写入新记忆。

LangGraph 的做法是"状态图"——每个节点有自己的状态,状态在节点之间传递。上下文管理变成了"状态管理"。好处是显式、可控;坏处是复杂度高,每个节点都要手动管理状态。

微软 Agent Framework (MAF) 的做法是"上下文压缩系统"(CompactionSystem)——当上下文超过窗口限制时,自动压缩历史消息。这个思路跟我的 CompactAsync 类似,但 MAF 的实现更精细——它会用 LLM 来总结旧消息,而不是简单删除。

AgentScope Java 走的是 RAG 路线——它有 5 个 RAG 扩展(百炼、Dify、RAGFlow、HayStack、pgvector),覆盖了从文档切片到向量搜索的完整链路。这是"用计算换检索"的典型做法。


关键认知

上下文工程的核心设计原则是:可组合、可扩展、可调试。

  • 上下文提供者链让不同来源可以独立扩展
  • 三层结构(L0/L1/L2)让信息按需加载
  • 降级策略让系统在缺少组件时依然能运行

Agent 的智能程度,不取决于模型有多强,取决于你能把多少正确的信息放进它的上下文窗口里。

上下文工程就是解决这个问题的:在有限的窗口里,放进最精准的信息。