sync: 鍚屾灞€鍩熺綉鏈€鏂颁唬鐮?(source master @ b69568e4)

This commit is contained in:
gozeros committed 2026-10-21 14:51:31 +08:00
1 parent 4f7f07504a
commit 8bc8b0e082
1651 files changed
+278599 -11416

No files matched your search

@@ -1,6 +1,59 @@
# Conversation Summarization
DeerFlow includes automatic conversation summarization to handle long conversations that approach model token limits. When enabled, the system automatically condenses older messages while preserving recent context.
DeerFlow includes automatic conversation summarization to handle long conversations that approach model token limits. When enabled, the system automatically condenses older messages while preserving recent context.
## Administrator recent-three-turn window
Administrators can enable a simpler deployment-wide sliding window in
「设置 → 上下文管理」. It is stored as
`system_settings.json -> conversation_context.recent_turn_window_enabled`, is
disabled by default, and is exposed through
`GET/PUT /api/system-settings/conversation-context` (write access is admin-only).
When enabled, every lead-agent model call receives only the latest three real
user turns, counting the current request, plus the assistant messages and tool
results that belong to those turns. Hidden business context immediately attached
to the first retained turn and system instructions are preserved. Trimming is
performed on `ModelRequest` only, so checkpoint history and the transcript shown
to the user remain complete.
This mode takes priority over the per-chat 「上下文压缩」 option and does not run
the summarization model. Turning it off restores the existing summarization
behavior without losing earlier messages.
## Hard input limit (always-on safety net)
Summarization is a quality feature and may be disabled by the user. A model can
also exceed its real window because the final request contains more than chat
history: the system prompt, tool schemas, hidden memory/RAG context, and
historical reasoning are all sent to the provider. For that reason DeerFlow
also supports a model-specific, always-on hard limit:
```yaml
models:
- name: qwen-local
use: langchain_openai:ChatOpenAI
model: Qwen3.8-27B
# This is the safe INPUT ceiling, not the model's advertised total window.
# For a 200k deployment, 160k leaves room for output and tokenizer variance.
max_input_tokens: 160000
max_tokens: 8192
```
`max_tokens` limits generated output. `max_input_tokens` limits the complete
input request and is consumed only by DeerFlow; it is never sent to the
OpenAI-compatible endpoint. The final model middleware conservatively counts
the system message, ordinary and hidden messages, tool schemas, response
format, tool calls/results, and historical reasoning. If the request is over
budget it transiently removes the oldest complete turns, drops expendable
hidden context, and truncates oversized tool/message bodies as a last resort.
The checkpoint and the conversation displayed in the UI are unchanged.
For LangChain-compatible configuration, the same limit may be expressed as
`profile.max_input_tokens`. A top-level `max_input_tokens` is preferred in this
project because it is visible alongside `max_tokens` in model configuration.
Do not set `max_tokens: 200000`: that asks the provider to generate up to 200k
tokens and does not protect the input context.
## Overview
@@ -19,7 +72,7 @@ The summarization feature uses DeerFlow's `DeerFlowSummarizationMiddleware` (a s
LangChain's stock `SummarizationMiddleware` runs in `before_model` and persists a `RemoveMessage(REMOVE_ALL_MESSAGES)` to the checkpoint — which permanently deletes the original Q&A from both the model context *and* the conversation the frontend renders. DeerFlow overrides this:
- `before_model` / `abefore_model` are **no-ops** (the destructive path is disabled).
- Compression runs in `wrap_model_call` / `awrap_model_call`: for a single model call, the message list is replaced with `[summary, *recent]` via `request.override(messages=...)`. **Nothing is written back to state.**
- Compression runs in `wrap_model_call` / `awrap_model_call`: for a single model call, the hidden summary is merged into LangChain's dedicated `ModelRequest.system_message` slot and the ordinary message list is replaced with the recent tail. This guarantees exactly one leading system message even for strict Qwen-style or generic `langchain_chat` adapters. **Nothing is written back to state.**
- The full original messages stay in the checkpoint, so the frontend keeps showing the user's real conversation. The summary message is named `summary` and flagged `hide_from_ui`, and because it only ever lives in the transient request it never reaches the frontend at all.
- The generated summary is cached **per-thread** so we don't pay an extra LLM call on every model invocation. The cached summary is reused (no LLM call) until enough *new* messages accumulate to push the compressed context back over the trigger threshold (a sticky boundary mirroring the stock middleware's post-compaction behavior). The cache is bounded (oldest threads evicted first).
- **Threshold-first**: while the full current conversation is still under the trigger, the request is passed through untouched — no cache lookup, no summary substitution. Compression only ever engages once the real context crosses the configured trigger.
@@ -185,7 +238,7 @@ The default LangChain prompt instructs the model to:
- Messages to summarize (older messages beyond the `keep` threshold)
- Messages to preserve (recent messages within the `keep` threshold)
4. **Summary Generation**: The model generates a concise summary of the older messages
5. **Context Replacement (transient)**: The message list **handed to the model for this call** is rebuilt as `[summary, *recent]`. The persisted state is left untouched — old messages are **not** removed, so the frontend still renders the full original conversation.
5. **Context Replacement (transient)**: The request **handed to the model for this call** is rebuilt with one system message (agent prompt + hidden summary) and the recent non-system tail. The persisted state is left untouched — old messages are **not** removed, so the frontend still renders the full original conversation.
6. **AI/Tool Pair Protection**: The system ensures AI messages and their corresponding tool messages stay together
7. **Skill Rescue**: Before the summary is generated, the most recently loaded skill files (tool results whose tool name is in `skill_file_read_tool_names` and whose target path is under `skills.container_path`) are lifted out of the summarization set and prepended to the preserved tail. Selection walks newest-first under three budgets: `preserve_recent_skill_count`, `preserve_recent_skill_tokens`, and `preserve_recent_skill_tokens_per_skill`. The triggering AIMessage and all of its paired ToolMessages move together so tool_call ↔ tool_result pairing stays intact.
@@ -202,12 +255,14 @@ The middleware intelligently preserves message context:
- **Recent Messages**: Always kept intact based on `keep` configuration
- **AI/Tool Pairs**: Never split - if a cutoff point falls within tool messages, the system adjusts to keep the entire AI + Tool message sequence together
- **Summary Format**: Summary is injected as a HumanMessage with the format:
```
Here is a summary of the conversation to date:
[Generated summary text]
```
- **Summary Format**: Summary is merged into the leading SystemMessage with the format:
```
Internal conversation summary for continuity only...
<internal_context_summary>
[generated summary text]
</internal_context_summary>
```
## Best Practices
@@ -329,7 +384,8 @@ Summarization runs after ThreadData and Sandbox initialization but before Title
- Configuration is loaded once at startup
- Summarization is **non-destructive**: the summary replaces older messages only in the transient model request (`wrap_model_call`), never in persisted state. The checkpointer therefore keeps the full original conversation
- The per-thread summary cache lives in a **process-wide, thread-safe LRU** (`_GLOBAL_SUMMARY_CACHE`, bounded), **not** on the middleware instance. `make_lead_agent` rebuilds a fresh middleware every run, so an instance cache would be cold each turn and re-run a blocking summary LLM call on every turn of a long conversation; the module-level cache keyed by `thread_id` lets a summary survive across turns. It is a cost optimization only — a cold/stale cache simply re-summarizes, which is always correct
- The per-thread summary cache lives in a **process-wide, thread-safe LRU** (`_GLOBAL_SUMMARY_CACHE`, bounded), **not** on the middleware instance. `make_lead_agent` rebuilds a fresh middleware every run, so an instance cache would be cold each turn and re-run a blocking summary LLM call on every turn of a long conversation; the module-level cache keyed by `thread_id` lets a summary survive across turns. It is a cost optimization only — a cold/stale cache simply re-summarizes, which is always correct.
- Compression normalizes LangChain's split request representation before invoking the next middleware/model: the dedicated `system_message` contains all system context exactly once, while `messages` contains no `SystemMessage`. This makes the fix independent of the configured provider adapter.
## Example Configurations