Agent 研习舱

chapter 04 / 上下文管理层

上下文工程

上下文是稀缺资源:腐化、焦虑、压缩与缓存

第 1 章说过:模型是无状态的,所有「记忆」都是 Harness 每轮喂进去的。这一章讲的就是「喂什么」这门手艺——上下文工程。它可能是 Harness 各层中投入产出比最高的一层。

上下文是稀缺资源

上下文窗口再大也是有限的,而且并非越满越好。Anthropic 的工程实践把这一点说得很直白:上下文是需要策展(curate)的注意力预算——系统提示、工具定义、示例、历史消息、检索结果都在争夺同一份预算。

Anthropic Engineering2025-09-29

Effective context engineering for AI agents

上下文是稀缺资源:如何在系统提示、工具、示例、检索之间策展有限的注意力预算——上下文工程是 Harness 的核心职责之一。

在循环模拟器里你已经见过:上下文只增不减。多轮文件读取、命令输出、日志打印天然膨胀,编码 Agent 尤其严重。不加管理的后果不是「变慢」,而是输出质量塌方

两种典型的失败模式

Context Rot:注意力被塞爆

Context Rot(上下文腐烂):Agent 被文档结构引发的探索行为吸引,打开几十个无关文档、塞入几十万 token,输出反而变差。两种常见触发:

  • 架构总览写太多 → Agent 想「更好地理解架构」,把相关文档全部展开;
  • 「别做」清单太长(30–50 条无替代方案)→ Agent 逐条检查是否违规,翻出迁移脚本、兼容层等一堆不相关代码。

Context Anxiety:模型自己先怂了

Context Anxiety(上下文焦虑):模型感知自己接近窗口极限时提前收工——到第 8 个子任务开始跳过测试、简化实现、写 TODO 注释。不是任务变难了,而是上下文太长了。

有意思的是:原地压缩不能完全解决它,因为模型仍然感知到「这是段长对话」。更有效的是上下文重置——完全清空 + 结构化交接文件,让新 Agent 在干净状态下继续(这也是 Ralph Loop 的思路:文件系统在窗口之间提供连续性)。

Harness 的应对工具箱

  • 上下文压缩:Clipping(限制单个工具输出的最大长度)+ Transcript Reduction(历史压成「近事浓、远事淡」的摘要)+ 去重(同一文件多次读取只留最近一次)。Claude Code 的 /compact 就是手动触发的压缩。
  • Prompt Prefix Caching:把稳定的前缀(系统提示、工具定义)缓存起来,每轮只为增量付费——这要求 Harness 组装上下文时保持前缀稳定,别随手往开头插东西。
  • 工作记忆与完整会话分离:窗口里只放工作记忆,完整历史放在窗口之外的会话对象里(Session as Context Object),需要时检索回来。
  • 长期记忆:跨会话的知识沉淀交给专门的记忆系统。openJiuwen 有独立的 agent-memory 仓库(jiuwen_memory/memory_core/long_term_memory.py),提供抽取、存储、图谱化检索。

Sebastian Raschka 的判断值得贴在墙上:"看起来是模型能力问题,实际上大多是 context quality 问题。"

源码走读:ContextEngine

openJiuwen 把上下文管理拆成了独立引擎——注意它的职责清单里没有一条是「生成回答」:

source walkthrough

ContextEngine:上下文的创建、压缩与持久化

openJiuwen-ai/agent-core @ 1e3a5c7a3d · openjiuwen/core/context_engine/context_engine.py:25-322 · 提取于 2026-07-18

openJiuwen 把上下文管理从 Agent 中拆出来做成了独立引擎:处理器链负责窗口截断与压缩,上下文状态随会话持久化。三段代码分别展示引擎的职责边界、主动压缩的结果语义、以及状态存档。

  1. 1L2539职责边界:注册处理器、创建隔离上下文、执行处理链
    class ContextEngine:
        """
        Manages the lifecycle and processing of conversational context.
    
        ContextEngine acts as the central entry-point for:
        1. Registering and configuring message processors.
        2. Creating isolated ModelContext instances tied to a session.
        3. Applying processor chains to enforce window limits, compression, etc.
    
        Parameters
        ----------
        config : ContextEngineConfig, optional
            Global engine settings (message/token limits, processor defaults).
            If omitted, a default configuration is used.
        """

    注意这个类的三条职责没有一条是「生成回答」——上下文管理是纯粹的资源管理问题:窗口限制、压缩策略都被抽象成可插拔的 processor 链。每个 ModelContext 与 session 绑定并相互隔离,这是防止「上下文污染」的结构性手段。

  2. 2L158187主动压缩:busy / compressed / noop 三种结果
    async def compress_context(
            self,
            context_id: str = "default_context_id",
            session: Session = None,
            *,
            session_id: str = None,
            processor_types: List[str] = None,
            **kwargs,
    ) -> str | dict[str, Any]:
        """
        Actively run registered compression processors for an existing context.
    
        Returns:
            Compression result code:
            - ``"busy"``: passive compression is already in progress.
            - ``"compressed"``: active compression ran and changed context.
            - ``"noop"``: active compression ran but nothing changed, or no
              compression processor is registered.
    
        A successful compression is saved to the resolved session and committed
        before this method returns. Contexts without a session remain in-memory only.
        """

    对应 Claude Code 的 /compact:压缩既可以被动触发(接近窗口上限时),也可以主动调用。三种返回值把并发语义讲得很清楚——被动压缩正在跑时返回 busy 而不是叠加执行;压缩成功后立即持久化并 commit。「压缩」是 Harness 预防 Context Anxiety 的主动手段。

  3. 3L284322状态存档:消息、窗口位置、token 统计一起保存
    @_fw.emit_after(ContextEvents.CONTEXT_OFFLOADED, result_key="result")
    async def save_contexts(self,
                            session: Session,
                            context_ids: List[str] = None
                            ):
        """
        Batch-persist multiple contexts and their runtime states.
    
        Each context's messages, sliding-window position, token count and statistics
        are saved locally.
        """
        if not session:
            context_engine_logger.warning(
                "Save context failed, session cannot be None",
                event_type=LogEventType.CONTEXT_SAVE,
            )
            return
        session_id = session.get_session_id()
        states = dict()
        if context_ids is None:
            context_ids = [
                context.context_id() for context_id, context in self._context_pool.items()
                if context.session_id() == session_id
            ]
    
        for context_id in context_ids:
            context_id = self._process_context_id(context_id)
            full_context_id = f"{session_id}_{context_id}"
            context = self._context_pool.get(full_context_id)
            if context is None or not hasattr(context, "save_state"):
                continue
            context_state = context.save_state()
            states[context_id] = context_state
        self._save_state_to_session(session, states)
        return states

    第 2 章循环里反复出现的 save_contexts 就是这里。存的不只是消息,还有滑动窗口位置和 token 统计——恢复时压缩策略能接着上次的状态算。装饰器 emit_after 发出 CONTEXT_OFFLOADED 事件,可观测性挂在事件总线上而不是散落在业务代码里。

以上为 Apache-2.0 许可的 openJiuwen 源码节选,仅截取教学所需片段;完整实现见 GitHub(固定 commit)

回看第 2 章的 ReAct 循环:每次模型调用前 get_context_window(...) 组装窗口、每轮结束后 save_contexts(...) 存档——现在你知道这两个调用背后是整个引擎在工作。

本章能力在 Harness 中处于什么位置?

harness positioning

本章能力在 Harness 中处于什么位置?

上下文管理层坐在循环驱动层与模型内核之间:循环每转一圈,它决定「这一轮模型看到什么」。它与状态与恢复层紧密协作(上下文状态要能存档重载),与评估层互为镜像(压缩摘要的质量本身需要评估)。

本章概念清单 / 点击进入概念卡片

章末测验

chapter quiz

0/4 已答

  1. Q1什么是 Context Rot(上下文腐烂)?

  2. Q2Context Anxiety(上下文焦虑)的表现是?

  3. Q3上下文压缩的两种最小可行策略是?

  4. Q4openJiuwen ContextEngine 的 compress_context 返回 "busy" 表示什么?