> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fim.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 上下文管理

> FIM One 如何管理对话上下文 — 一个五层纵深防御系统，可防止令牌溢出，同时保持对话质量。

## 问题所在

LLM 的上下文窗口是有限的。一个 128K 令牌的模型听起来很慷慨，直到你减去输出预算、系统提示、工具描述和多轮对话的累积历史。长对话、大型工具结果和多步骤智能体循环都会对这个限制造成压力——通常在单个会话内就会发生。

天真的解决方案是截断：当窗口填满时丢弃旧消息。这很快且可预测，但它会无差别地破坏上下文。用户的原始意图、早期轮次的关键决策和关键数据点在生硬的字符截断时都会消失。相反的极端——在每个轮次上进行 LLM 驱动的摘要——保留了语义内容，但成本高昂、速度慢，并引入了自己的故障模式（幻觉摘要、数值精度丧失）。

真正的挑战不是"适应窗口"。而是：**优雅地降级而不丧失关键信息，不在不必要的压缩上浪费令牌，也不增加用户能感受到的延迟。**

FIM One 通过五层纵深防御架构解决了这个问题。每一层解决问题的不同规模，它们组合得很干净——没有单一层需要完美，因为下一层会捕捉它遗漏的内容。

## 五层防御机制

上下文管理不是单一机制。它是一个堆栈，其中每一层在特定的粒度级别处理特定的关注点：

| 层级    | 组件           | 功能                | 触发时机         |
| ----- | ------------ | ----------------- | ------------ |
| **5** | 预算配置         | 从模型规格计算可用的输入令牌预算  | 启动时 / 每次请求   |
| **4** | DbMemory     | 加载持久化历史记录，加载时进行压缩 | 每次请求一次       |
| **3** | ContextGuard | 每次迭代的预算强制执行       | 每个 ReAct 迭代  |
| **2** | CompactUtils | 令牌估计、智能截断、LLM 压缩  | 由第 3 和 4 层调用 |
| **1** | 内存实现         | 抽象接口 + 具体策略       | 框架级别         |

这些层从下到上编号，因为高层依赖于低层。第 5 层设置预算。第 4 层进行初始加载时压缩。第 3 层在每次迭代时强制执行预算。第 2 层和第 1 层提供第 3 层和第 4 层使用的原语。

```mermaid theme={null}
flowchart TD
    L5["Layer 5: Budget Configuration<br/><i>context_size - max_output</i>"]
    L4["Layer 4: DbMemory<br/><i>Load history, compact on load</i>"]
    L3["Layer 3: ContextGuard<br/><i>Per-iteration enforcement</i>"]
    L2["Layer 2: CompactUtils<br/><i>Estimation, truncation, LLM compact</i>"]
    L1["Layer 1: Memory Implementations<br/><i>BaseMemory, WindowMemory, SummaryMemory, DbMemory</i>"]

    L5 -->|"budget"| L4
    L5 -->|"budget"| L3
    L4 -->|"calls"| L2
    L3 -->|"calls"| L2
    L4 -.->|"implements"| L1
```

### 第 5 层 — 预算配置

预算由两个值计算得出：

```
usable_input_tokens = context_size - max_output_tokens
```

默认值为：`128,000 - 64,000 = 64,000 tokens`。

不会减去静态系统提示预留。系统提示（智能体指令、工具描述、格式化开销）包含在 ContextGuard 动态估计的消息列表中 — 在顶部减去固定预留会导致重复计算并浪费预算。典型的系统提示消耗 3K-5K 个 token；具有许多工具或长指令的智能体可能会使用更多。ContextGuard 会自动考虑实际大小。

预算值可以来自三个来源，按优先级顺序解决：

1. **数据库 ModelConfig** — 由管理员设置的每个模型 `context_size` 和 `max_output_tokens`。
2. **环境变量** — `LLM_CONTEXT_SIZE` 和 `LLM_MAX_OUTPUT_TOKENS`。
3. **硬编码默认值** — 128K 上下文，64K 输出。

主 LLM 和快速 LLM 具有独立的预算。DAG 步骤执行使用快速 LLM 的预算；ReAct 模式使用主 LLM 的预算。这很重要，因为运营商通常会为 ReAct 配对一个大上下文模型（历史记录会累积），为 DAG 步骤配对一个更小、更快的模型（每个步骤都从头开始）。

强制执行 4,000 token 的下限 — 如果配置错误的值会产生更小的预算，系统会固定到 4K，而不是无声地失败。

### Layer 4 — DbMemory

`DbMemory` 是生产环境中的内存实现。它从数据库加载持久化的对话历史，并将其压缩以适应令牌预算，然后才会被智能体看到。

该设计**有意设为只读**。持久化由 `chat.py` 处理——拥有完整消息生命周期的 API 层（包括元数据、使用情况跟踪和图像附件）。`DbMemory` 仅读取。其 `add_message()` 和 `clear()` 方法是空操作。这种分离防止了双重写入，并将持久化逻辑保持在一个地方。

加载时，`DbMemory`：

1. 查询对话的所有 `user` 和 `assistant` 消息，按创建时间排序。
2. 删除尾部用户消息（当前查询，智能体将重新附加）。
3. 重建图像附件——包含图像的用户消息在数据库中存储元数据（`file_id`、`mime_type`），`DbMemory` 从磁盘重建 base64 数据 URL，以便 LLM 可以"看到"之前回合的图像。
4. 压缩：如果提供了 `compact_llm`，使用 `CompactUtils.llm_compact()`。否则，回退到 `CompactUtils.smart_truncate()`。

压缩后，`DbMemory` 设置跟踪标志（`was_compacted`、`_original_count`、`_compacted_count`），SSE 层使用这些标志向前端发出 `compact` 事件。

### 第 3 层 — ContextGuard

`ContextGuard` 是每次迭代的预算执行器。它在每个 ReAct 迭代的顶部被调用——既在独立 ReAct 模式中，也在每个 DAG 步骤的智能体内部。这是消息到达 LLM API 之前的最后一道防线。

执行遵循三步流程：

1. **截断超大单条消息。** 任何超过 50K 字符的单条消息都会被硬截断并添加 `[Truncated]` 后缀。这可以捕获失控的工具输出——返回整个网页的网页抓取、转储大型数据集的文件读取。

2. **估计总令牌数。** 如果总数在预算范围内，立即返回。大多数迭代在这里通过——压缩是例外，而不是常态。

3. **如果超过预算则压缩。** 如果有可用的 `compact_llm`，使用带有提示特定提示的 LLM 驱动压缩。否则，回退到 `smart_truncate`。

**提示系统**使 ContextGuard 具有上下文感知能力，而不是一刀切的方案。不同的情况需要不同的压缩策略：

| 提示                | 使用者         | 保留               | 丢弃                   |
| ----------------- | ----------- | ---------------- | -------------------- |
| `react_iteration` | ReAct 智能体循环 | 最近的推理链、当前目标、关键数据 | 旧的冗余步骤、失败的重试、冗长的工具输出 |
| `planner_input`   | DAG 丰富查询    | 用户意图演变、关键决策、约束条件 | 对话细节、问候语、工具调用机制      |
| `step_dependency` | DAG 步骤上下文   | 关键数据、数字、结论       | 推理过程、失败的尝试、冗长的格式     |
| `general`         | 默认回退        | 关键事实、决策、工具结果     | 问候语、填充内容、冗余的往返       |

每个提示都映射到一个精心措辞的系统提示，告诉压缩 LLM 要保留什么和要丢弃什么。提示以"用与对话相同的语言写作"结尾——这对于摘要会被默认转换为英文的 CJK 用户来说很重要。

如果 LLM 压缩失败（网络错误、空响应、任何异常），ContextGuard 会无声地回退到 `smart_truncate`。智能体永远看不到失败。这是一个刻意的可靠性选择：通过启发式截断丢失一些上下文比崩溃迭代要好。

### Layer 2 — CompactUtils

`CompactUtils` 是一个无状态的实用工具类——没有实例，没有状态，只有纯函数。它提供了第 3 层和第 4 层构建的三种能力。

**Token 估算**将文本转换为近似的 token 计数，无需导入 tokenizer 库。启发式算法：

* ASCII 字符：\~4 个字符每 token
* CJK / 非 ASCII 字符：\~1.5 个字符每 token
* 图像：765 tokens 每张图像（固定成本）
* 每条消息开销：4 tokens（角色标记、分隔符）

**`smart_truncate`** 是启发式回退方案。它无条件地保留固定消息，然后向后遍历非固定消息，累积直到预算耗尽。结果是适合的对话后缀。它还确保结果永远不会以助手消息开头——孤立的助手轮次（前面没有用户消息）会让 LLM 感到困惑。

**`llm_compact`** 是 LLM 驱动的路径。它将消息分为三组——系统消息（始终保留）、固定消息（始终保留）和可压缩消息。最旧的可压缩消息被总结为单个 `[Conversation summary]` 系统消息；最近的 4 条消息保持原样。如果压缩后的结果仍然过长，它会在压缩输出上回退到 `smart_truncate`——双重保险。

### 第 1 层 — 内存实现

内存层定义了 `BaseMemory` 接口：`add_message()`、`get_messages()`、`clear()`。存在三种实现：

* **WindowMemory** — 基于计数的滑动窗口。保留最后 N 条非系统消息。简单、可预测，无 LLM 调用。不用于生产环境；适用于测试和无状态场景。

* **SummaryMemory** — 当消息计数超过阈值时触发 LLM 摘要化。将旧消息压缩为 `[Conversation summary]` 系统消息。不用于生产环境；早于更复杂的 ContextGuard 方法。

* **DbMemory** — 生产实现（在第 4 层中描述）。数据库支持，只读，在加载时使用 LLM 或启发式压缩。

WindowMemory 和 SummaryMemory 保留在代码库中，因为它们作为测试的有用基元，以及对于嵌入 FIM One 核心库而不使用 Web 层的用户很有用。它们不是死代码 — 它们是架构发展而来的简单情况。

## ReAct 中的上下文流动

ReAct 智能体在两个不同的阶段使用上下文管理：加载时和迭代时。

```mermaid theme={null}
sequenceDiagram
    participant Chat as chat.py
    participant Db as DbMemory
    participant Agent as ReActAgent
    participant CG as ContextGuard
    participant LLM as LLM API

    Chat->>Db: get_messages(budget)
    Db->>Db: Load from DB, compact
    Db-->>Chat: history (within budget)

    Chat->>Agent: run(query)
    Agent->>Agent: Assemble: system + history + pinned query

    loop Each iteration (non-streaming)
        Agent->>CG: check_and_compact(messages, hint="react_iteration")
        CG->>CG: Truncate oversized, estimate, compact if needed
        CG-->>Agent: messages (within budget)
        Agent->>LLM: chat(messages)
        LLM-->>Agent: response
        alt tool_call
            Agent->>Agent: Execute tool, append observation
            opt cycle detected
                Agent->>Agent: Inject warning message into context
            end
        else final_answer
            opt tools ≥ threshold, not yet checked
                Agent->>Agent: Inject checklist prompt into context
                Agent->>Agent: Continue loop (one more iteration)
            end
        end
    end

    Agent-->>Chat: AgentResult (tool trace + brief answer)
    Chat->>Agent: stream_answer(query, result)
    Agent->>LLM: stream_chat(synthesis prompt)
    LLM-->>Agent: token stream
    Agent-->>Chat: streamed answer → SSE to frontend
    Chat->>Chat: Persist, emit compact SSE if compacted
```

工具迭代使用非流式 `chat()` 以提高速度；答案合成使用通过 `stream_answer()` 的流式 `stream_chat()`。这种两阶段分割——快速工具循环后跟流式合成——优化了延迟和用户体验。有关包括双模式执行和工具选择的完整 ReAct 引擎架构，请参阅 [ReAct 引擎](/architecture/react-engine)。

关键洞察：**DbMemory 处理历史上下文问题（来自先前请求的轮次），而 ContextGuard 处理请求内增长问题（工具结果在智能体循环期间累积）。** 它们在不同的时间尺度上运行并捕获不同的故障模式。

用户的当前查询始终标记为 `pinned=True`。这确保它在所有压缩中都能保留——`smart_truncate` 和 `llm_compact` 都无条件地保留固定消息。无论历史记录压缩得多么激进，用户的实际问题永远不会丢失。

## 上下文如何通过 DAG 流动

DAG 模式的上下文形状与 ReAct 根本不同。它不是一个长对话线程，而是一棵树：一个规划阶段、多个并行执行步骤和一个分析阶段。每个阶段都有自己的上下文管理策略。

```mermaid theme={null}
flowchart TD
    A["1. Load history via DbMemory"] --> B["2. Build enriched_query"]
    B --> C{"enriched_query > 16K tokens?"}
    C -->|Yes| D["LLM summarize with planner_input prompt"]
    C -->|No| E["Use as-is"]
    D --> F["3. DAGPlanner (structured_llm_call)"]
    E --> F
    F --> G["4. Execution: parallel steps"]
    G --> H["Step 1: independent ReAct"]
    G --> I["Step 2: independent ReAct"]
    G --> J["Step N: independent ReAct"]
    H --> K["5. PlanAnalyzer (structured_llm_call)"]
    I --> K
    J --> K
    K --> L{"Achieved?"}
    L -->|No, low confidence| M["6. Replan: 500-char step summaries"]
    M --> F
    L -->|Yes| N["stream_synthesize → Final answer"]
```

**阶段 1 — 历史记录加载。** DbMemory 加载并压缩对话历史记录，与 ReAct 相同。压缩后的历史记录被格式化为以 `"Previous conversation:"` 为前缀的文本块。

**阶段 2 — 富化查询构造。** 历史记录文本和当前查询被组合成一个 `enriched_query`。如果超过 16K 个令牌，将使用 `planner_input` 提示词进行 LLM 总结。选择 16K 阈值是因为规划器需要一次性读取整个查询 — 与 ReAct 不同，规划期间没有迭代压缩。

**阶段 3 — 规划。** 规划器接收一个 2 条消息的提示：系统提示加富化查询。这里没有 ContextGuard — 富化查询已经通过 16K 检查进行了大小控制。

**阶段 4 — 步骤执行。** 每个 DAG 步骤作为一个独立的 ReAct 智能体运行，具有自己的 ContextGuard。关键是，这些步骤智能体**没有记忆** — 它们从零开始，只有任务描述和依赖上下文。这是设计使然：DAG 步骤应该是自包含的工作单元。依赖结果通过 `_build_step_context` 注入，该函数在 50K 处进行字符截断（ContextGuard 的 `max_message_chars` 限制）。

**阶段 5 — 分析。** 步骤结果被格式化供分析器 LLM 使用，每个步骤在 10K 字符处进行截断。这可以防止单个步骤的冗长输出主导分析上下文。

**阶段 6 — 重新规划。** 当分析器确定目标未实现且置信度低于阈值时，步骤结果被截断为仅 500 个字符用于重新规划上下文。重新规划需要知道*发生了什么*和*哪里出错了*，而不是每个步骤输出的完整细节。这种激进的截断使重新规划提示足够紧凑，以便规划器能够有效处理。

有关完整的 DAG 管道架构（包括 LLM 调用映射和重新规划逻辑），请参阅 [DAG 引擎](/architecture/dag-engine)。

## 固定消息

固定机制防止压缩破坏必须保留的消息。有两类消息被固定：

1. **当前用户查询** — 始终固定。如果用户提出问题且历史记录过长，系统会压缩历史记录，而不是问题。

2. **中途注入的消息** — 当用户在智能体仍在运行时发送后续消息时，注入的消息被标记为固定，以便智能体在下一次迭代中看到它。

固定的风险在于积累。在有许多注入消息的长会话中，固定内容可能会增长到消耗大部分预算，导致实际对话历史没有空间。ContextGuard 通过硬上限来解决这个问题：**当固定令牌超过预算的 50% 时，最旧的注入消息被取消固定并移到可压缩池。** 只有最近的固定消息（当前查询）被保留。

这是一种权衡。取消固定旧的注入消息意味着它们可能被总结或截断。但另一种选择——让固定消息挤占所有其他上下文——更糟。系统倾向于保留最近的上下文，这几乎总是比较旧的注入更相关。

## 令牌估计

FIM One 使用启发式令牌估计而非真实分词器。这是一个经过深思熟虑的选择，具有明确的权衡。

**为什么不使用真实分词器？** 三个原因：

1. **依赖成本。** `tiktoken`（OpenAI 的分词器）是 15MB 的编译 Rust 绑定。`sentencepiece`（某些开源模型使用）有其自身的构建要求。对于针对多个 LLM 提供商的框架，没有单一正确的分词器——每个模型族都使用不同的分词器。

2. **速度。** 启发式估计是对字符串的单次遍历。真实分词涉及词汇表查找、BPE 合并操作和特殊令牌处理。ContextGuard 在每次迭代中调用估计，有时多次——速度差异很重要。

3. **足够好。** 启发式方法针对混合语言文本进行了调整（ASCII/CJK 分割涵盖两个主要情况）。对于边界情况（大量标点符号的代码、不寻常的 Unicode），可能偏差 1.5-2 倍，但上下文管理本质上是近似的。在 64K 预算上偏差 30% 仍然留有舒适的余量。

具体启发式方法：

| 内容类型          | 比率          | 原理                                        |
| ------------- | ----------- | ----------------------------------------- |
| ASCII 文本      | \~4 字符/令牌   | 英文散文和代码在 GPT/Claude 分词器中平均为 3.5-4.5 字符/令牌 |
| CJK / 非 ASCII | \~1.5 字符/令牌 | 每个 CJK 字符通常为 1-2 个令牌；1.5 是几何平均值           |
| 图像            | 765 令牌/图像   | 视觉 API 中 base64 编码图像的近似成本                 |
| 每条消息开销        | 4 令牌        | 角色标记、分隔符、格式化                              |

估计总是为非空内容返回至少 1 个令牌。这可防止预算算术中的除以零边界情况。

## 用户看到的内容

上下文管理的设计目标是在常见情况下保持隐形，仅在激活时产生最小干扰。用户可见的信号包括：

**CompactDivider。** 当 `DbMemory` 在加载时压缩历史记录时，前端会渲染一条虚线分隔符，显示文本"较早的上下文（N 条消息）已由 AI 总结。"这会出现在摘要和保留的最近消息之间，为用户提供视觉提示，表明较旧的上下文已被压缩，而不会中断对话流。

**令牌使用显示。** 每个响应末尾的 `done` 卡片显示"X.Xk in / X.Xk out"——消耗的总输入和输出令牌。这包括在压缩上花费的令牌（用于总结的快速 LLM 调用）。监控令牌消耗的用户可以看到压缩何时增加了开销。

**优雅的错误处理。** 如果上下文管理完全失败——考虑到回退链，这种情况不应该发生，但理论上可能发生——错误会作为响应中的智能体错误文本出现，而不是系统崩溃。对话继续进行；用户可以重试或重新表述。

目标是大多数用户永远不需要考虑上下文管理。他们进行长对话，系统透明地处理预算，唯一可见的工件是偶尔出现的压缩分隔符。对于关心令牌效率的高级用户和运营人员，使用显示和可配置的预算参数提供了他们需要的控制。
