【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(6)Context Compact (上下文压缩)

简介: 本文介绍AI Agent的三层上下文压缩机制,解决长期运行时的token膨胀问题。包括微观压缩(替换旧工具结果为占位符)、自动压缩(超阈值时生成摘要)和手动压缩(LLM主动触发)。通过保存完整对话到磁盘并生成结构化摘要,实现无限会话能力。

第六章 Context Compact (上下文压缩)

s01 > s02 > s03 > s04 > s05 > [ s06 ] | s07 > s08 > s09 > s10 > s11 > s12

“本专栏基于开源项目 learn-claude-code 的官方文档。原文档非常硬核,为了方便像我一样的新手小白理解,我对文档进行了逐行精读,并加入了很多中文注释、大白话解释和踩坑记录。希望这套‘咀嚼版’教程能帮你推开 AI Agent 开发的大门。”

项目地址:shareAI-lab/learn-claude-code: Bash is all you need - A nano Claude Code–like agent, built from 0 to 1

"上下文总会满, 要有办法腾地方" -- 三层压缩策略, 换来无限会话。

一、问题-上下文窗口膨胀导致的长期运行能力限制

上下文窗口是有限的。读一个 1000 行的文件就吃掉 ~4000 token; 读 30 个文件、跑 20 条命令, 轻松突破 100k token。不压缩, 智能体根本没法在大项目里干活。

在之前的版本中,智能体采用的都是简单的消息累积模式,也就是每次工具调用的结果都完整保存在对话历史中;随着交互轮次的增加,messages列表会不断的增长;当token数量在接近LLM上下文窗口限制的时候,就会使它性能急剧下降、关键信息丢失以至于最后无法正常工作。

二、解决方案

三层压缩, 激进程度递增:

Every turn:
+------------------+
| Tool call result |
+------------------+
        |
        v
[Layer 1: micro_compact]        (silent, every turn)
  Replace tool_result > 3 turns old
  with "[Previous: used {tool_name}]"
        |
        v
[Check: tokens > 50000?]
   |               |
   no              yes
   |               |
   v               v
continue    [Layer 2: auto_compact]
              Save transcript to .transcripts/
              LLM summarizes conversation.
              Replace all messages with [summary].
                    |
                    v
            [Layer 3: compact tool]
              Model calls compact explicitly.
              Same summarization as auto_compact.

Layer 1: micro_compact(微观压缩)

  • 触发时机:每轮对话前自动执行
  • 压缩策略:保留最近3次工具调用的完整结果,更早的结果替换为占位符[Previous: used {tool_name}]
  • 优势:轻量级、无感知,显著减少重复工具输出的token消耗

Layer 2: auto_compact(自动压缩)

  • 触发条件:当估算token数超过50,000阈值时
  • 压缩流程
    1. 将完整对话历史持久化到.transcripts/目录
    2. 调用LLM对整个对话进行智能摘要
    3. 用摘要替换原有消息列表,仅保留关键信息
  • 优势:自动化处理,确保智能体永远不会因上下文过长而失效

Layer 3: compact tool(手动压缩)

  • 触发方式:智能体主动调用compact工具
  • 使用场景:当智能体意识到需要重置上下文或聚焦特定任务时
  • 优势:赋予智能体主动管理上下文的能力,实现更精细的控制

三、工作原理

  1. 第一层 -- micro_compact: 每次 LLM 调用前, 将旧的 tool result 替换为占位符。通过四个阶段实现智能上下文压缩:1、扫描并收集所有工具调用结果;2、然后基于保留策略判断是否需要压缩;3、构建工具ID到名称的映射关系以维持语义完整性;4、将冗余的历史结果替换为简洁的占位符,从而在保持对话连贯性的同时显著减少token消耗。
def micro_compact(messages: list) -> list:
    # 第一阶段:收集所有工具结果
    tool_results = []
    for i, msg in enumerate(messages):
        if msg["role"] == "user" and isinstance(msg.get("content"), list):
            for j, part in enumerate(msg["content"]):
                if isinstance(part, dict) and part.get("type") == "tool_result":
                    tool_results.append((i, j, part))
    # 第二阶段:早期退出条件
    if len(tool_results) <= KEEP_RECENT: # 当工具结果数量不超过保留阈值(默认3个)时,无需压缩
        return messages
    # 第三阶段:构建工具名称映射表
    tool_name_map = {
   }
    for msg in messages:
        if msg["role"] == "assistant":
            content = msg.get("content", [])
            if isinstance(content, list):
                for block in content:
                    if hasattr(block, "type") and block.type == "tool_use":
                        tool_name_map[block.id] = block.name
    # 第四阶段:执行压缩替换
    to_clear = tool_results[:-KEEP_RECENT]
    for _, _, result in to_clear:
        if isinstance(result.get("content"), str) and len(result["content"]) > 100:
            tool_id = result.get("tool_use_id", "")
            tool_name = tool_name_map.get(tool_id, "unknown")
            result["content"] = f"[Previous: used {tool_name}]"
    return messages
  1. 第二层 -- auto_compact: token 超过阈值时, 保存完整对话到磁盘, 让 LLM 做摘要。当token估算超过阈值时,自动触发全局压缩。通过三个阶段实现完整的对话历史压缩:1、持久化完整对话历史到.transcripts/目录,确保数据安全和可恢复性;2、调用LLM生成结构化摘要,明确要求包含已完成工作、当前状态和关键决策三个维度;3、用仅包含摘要和状态确认的2条消息替换原始历史,同时保留transcript文件路径以维持可追溯性,从而在保证任务连续性的同时极大降低token消耗。
def auto_compact(messages: list) -> list:
    # Save full transcript to disk
    TRANSCRIPT_DIR.mkdir(exist_ok=True)
    transcript_path = TRANSCRIPT_DIR / f"transcript_{int(time.time())}.jsonl"
    with open(transcript_path, "w") as f:
        for msg in messages:
            f.write(json.dumps(msg, default=str) + "\n")
    print(f"[transcript saved: {transcript_path}]")
    # Ask LLM to summarize
    conversation_text = json.dumps(messages, default=str)[:80000]
    response = client.messages.create(
        model=MODEL,
        messages=[{
   "role": "user", "content":
            "Summarize this conversation for continuity. Include: "
            "1) What was accomplished, 2) Current state, 3) Key decisions made. "
            "Be concise but preserve critical details.\n\n" + conversation_text}],
        max_tokens=2000,
    )
    summary = response.content[0].text
    # Replace all messages with compressed summary
    return [
        {
   "role": "user", "content": f"[Conversation compressed. Transcript: {transcript_path}]\n\n{summary}"},
        {
   "role": "assistant", "content": "Understood. I have the context from the summary. Continuing."},
    ]
  1. 第三层 -- manual compact: compact 工具按需触发同样的摘要机制。这里的manual不是指人工手动调用,而是指由LLM主动调用。不像auto_compact那样基于token阈值自动触发,而是由智能体根据任务需要决定何时压缩。
# 检测是否调用了compact工具
manual_compact = False
for block in response.content:
    if block.type == "tool_use":
        if block.name == "compact":
            manual_compact = True # 标记需要压缩
            output = "Compressing..."

# 在工具执行完成后检查是否需要手动压缩
if manual_compact:
    print("[manual compact]")
    messages[:] = auto_compact(messages)
  1. 循环整合三层:
def agent_loop(messages: list):
    while True:
        micro_compact(messages)                        # Layer 1
        if estimate_tokens(messages) > THRESHOLD:
            messages[:] = auto_compact(messages)       # Layer 2
        response = client.messages.create(...)
        # ... tool execution ...
        if manual_compact:
            messages[:] = auto_compact(messages)       # Layer 3

完整历史通过 transcript 保存在磁盘上。信息没有真正丢失, 只是移出了活跃上下文。

四、相对 s05 的变更

组件 之前 (s05) 之后 (s06)
Tools 5 5 (基础 + compact)
上下文管理 三层压缩
Micro-compact 旧结果 -> 占位符
Auto-compact token 阈值触发
Transcripts 保存到 .transcripts/

五、试一试

cd learn-claude-code
python agents/s06_context_compact.py

试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):

  1. Read every Python file in the agents/ directory one by one (观察 micro-compact 替换旧结果)
  2. Keep reading files until compression triggers automatically
  3. Use the compact tool to manually compress the conversation

六、完整代码

#!/usr/bin/env python3
import json
import os
import subprocess
import time
from pathlib import Path

from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv(override=True)

if os.getenv("ANTHROPIC_BASE_URL"):
    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)

WORKDIR = Path.cwd()
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]

SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks."

THRESHOLD = 50000
TRANSCRIPT_DIR = WORKDIR / ".transcripts"
KEEP_RECENT = 3


def estimate_tokens(messages: list) -> int:
    """Rough token count: ~4 chars per token."""
    return len(str(messages)) // 4


# -- Layer 1: micro_compact - replace old tool results with placeholders --
def micro_compact(messages: list) -> list:
    # Collect (msg_index, part_index, tool_result_dict) for all tool_result entries
    # 第一阶段:收集所有工具结果
    tool_results = []
    for msg_idx, msg in enumerate(messages):
        if msg["role"] == "user" and isinstance(msg.get("content"), list):
            for part_idx, part in enumerate(msg["content"]):
                if isinstance(part, dict) and part.get("type") == "tool_result":
                    tool_results.append((msg_idx, part_idx, part))
    # 第二阶段:早期退出条件
    if len(tool_results) <= KEEP_RECENT: # 当工具结果数量不超过保留阈值(默认3个)时,无需压缩
        return messages
    # Find tool_name for each result by matching tool_use_id in prior assistant messages
    # 第三阶段:构建工具名称映射表
    tool_name_map = {
   }
    for msg in messages:
        if msg["role"] == "assistant":
            content = msg.get("content", [])
            if isinstance(content, list):
                for block in content:
                    if hasattr(block, "type") and block.type == "tool_use":
                        tool_name_map[block.id] = block.name
    # Clear old results (keep last KEEP_RECENT)
    # 第四阶段:执行压缩替换
    to_clear = tool_results[:-KEEP_RECENT]
    for _, _, result in to_clear:
        if isinstance(result.get("content"), str) and len(result["content"]) > 100:
            tool_id = result.get("tool_use_id", "")
            tool_name = tool_name_map.get(tool_id, "unknown")
            result["content"] = f"[Previous: used {tool_name}]"
    return messages


# -- Layer 2: auto_compact - save transcript, summarize, replace messages --
def auto_compact(messages: list) -> list:
    # Save full transcript to disk
    TRANSCRIPT_DIR.mkdir(exist_ok=True)
    transcript_path = TRANSCRIPT_DIR / f"transcript_{int(time.time())}.jsonl"
    with open(transcript_path, "w") as f:
        for msg in messages:
            f.write(json.dumps(msg, default=str) + "\n")
    print(f"[transcript saved: {transcript_path}]")
    # Ask LLM to summarize
    conversation_text = json.dumps(messages, default=str)[:80000]
    response = client.messages.create(
        model=MODEL,
        messages=[{
   "role": "user", "content":
            "Summarize this conversation for continuity. Include: "
            "1) What was accomplished, 2) Current state, 3) Key decisions made. "
            "Be concise but preserve critical details.\n\n" + conversation_text}],
        max_tokens=2000,
    )
    summary = response.content[0].text
    # Replace all messages with compressed summary
    return [
        {
   "role": "user", "content": f"[Conversation compressed. Transcript: {transcript_path}]\n\n{summary}"},
        {
   "role": "assistant", "content": "Understood. I have the context from the summary. Continuing."},
    ]


# -- Tool implementations --
def safe_path(p: str) -> Path:
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path

def run_bash(command: str) -> str:
    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
    if any(d in command for d in dangerous):
        return "Error: Dangerous command blocked"
    try:
        r = subprocess.run(command, shell=True, cwd=WORKDIR,
                           capture_output=True, text=True, timeout=120)
        out = (r.stdout + r.stderr).strip()
        return out[:50000] if out else "(no output)"
    except subprocess.TimeoutExpired:
        return "Error: Timeout (120s)"

def run_read(path: str, limit: int = None) -> str:
    try:
        lines = safe_path(path).read_text().splitlines()
        if limit and limit < len(lines):
            lines = lines[:limit] + [f"... ({len(lines) - limit} more)"]
        return "\n".join(lines)[:50000]
    except Exception as e:
        return f"Error: {e}"

def run_write(path: str, content: str) -> str:
    try:
        fp = safe_path(path)
        fp.parent.mkdir(parents=True, exist_ok=True)
        fp.write_text(content)
        return f"Wrote {len(content)} bytes"
    except Exception as e:
        return f"Error: {e}"

def run_edit(path: str, old_text: str, new_text: str) -> str:
    try:
        fp = safe_path(path)
        content = fp.read_text()
        if old_text not in content:
            return f"Error: Text not found in {path}"
        fp.write_text(content.replace(old_text, new_text, 1))
        return f"Edited {path}"
    except Exception as e:
        return f"Error: {e}"


TOOL_HANDLERS = {
   
    "bash":       lambda **kw: run_bash(kw["command"]),
    "read_file":  lambda **kw: run_read(kw["path"], kw.get("limit")),
    "write_file": lambda **kw: run_write(kw["path"], kw["content"]),
    "edit_file":  lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
    "compact":    lambda **kw: "Manual compression requested.",
}

TOOLS = [
    {
   "name": "bash", "description": "Run a shell command.",
     "input_schema": {
   "type": "object", "properties": {
   "command": {
   "type": "string"}}, "required": ["command"]}},
    {
   "name": "read_file", "description": "Read file contents.",
     "input_schema": {
   "type": "object", "properties": {
   "path": {
   "type": "string"}, "limit": {
   "type": "integer"}}, "required": ["path"]}},
    {
   "name": "write_file", "description": "Write content to file.",
     "input_schema": {
   "type": "object", "properties": {
   "path": {
   "type": "string"}, "content": {
   "type": "string"}}, "required": ["path", "content"]}},
    {
   "name": "edit_file", "description": "Replace exact text in file.",
     "input_schema": {
   "type": "object", "properties": {
   "path": {
   "type": "string"}, "old_text": {
   "type": "string"}, "new_text": {
   "type": "string"}}, "required": ["path", "old_text", "new_text"]}},
    {
   "name": "compact", "description": "Trigger manual conversation compression.",
     "input_schema": {
   "type": "object", "properties": {
   "focus": {
   "type": "string", "description": "What to preserve in the summary"}}}},
]


def agent_loop(messages: list):
    while True:
        # Layer 1: micro_compact before each LLM call
        micro_compact(messages)
        # Layer 2: auto_compact if token estimate exceeds threshold
        if estimate_tokens(messages) > THRESHOLD:
            print("[auto_compact triggered]")
            messages[:] = auto_compact(messages)
        response = client.messages.create(
            model=MODEL, system=SYSTEM, messages=messages,
            tools=TOOLS, max_tokens=8000,
        )
        messages.append({
   "role": "assistant", "content": response.content})
        if response.stop_reason != "tool_use":
            return
        results = []
        manual_compact = False
        for block in response.content:
            if block.type == "tool_use":
                if block.name == "compact":
                    manual_compact = True
                    output = "Compressing..."
                else:
                    handler = TOOL_HANDLERS.get(block.name)
                    try:
                        output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
                    except Exception as e:
                        output = f"Error: {e}"
                print(f"> {block.name}: {str(output)[:200]}")
                results.append({
   "type": "tool_result", "tool_use_id": block.id, "content": str(output)})
        messages.append({
   "role": "user", "content": results})
        # Layer 3: manual compact triggered by the compact tool
        if manual_compact:
            print("[manual compact]")
            messages[:] = auto_compact(messages)


if __name__ == "__main__":
    history = []
    while True:
        try:
            query = input("\033[36ms06 >> \033[0m")
        except (EOFError, KeyboardInterrupt):
            break
        if query.strip().lower() in ("q", "exit", ""):
            break
        history.append({
   "role": "user", "content": query})
        agent_loop(history)
        response_content = history[-1]["content"]
        if isinstance(response_content, list):
            for block in response_content:
                if hasattr(block, "text"):
                    print(block.text)
        print()
目录
相关文章
|
4月前
|
人工智能 前端开发 Shell
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(1)The Agent Loop (智能体循环)
本专栏精讲AI智能体核心——Agent Loop循环机制,基于开源项目learn-claude-code,用不到50行Python代码实现ReAct范式(思考→行动→观察)。含逐行中文注释、踩坑记录与Bash工具实战,助新手快速入门AI Agent开发。(239字)
1811 2
|
4月前
|
消息中间件 人工智能 安全
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(8)Background Tasks (后台任务)
本节详解s08后台任务机制:通过守护线程异步执行`npm install`等耗时命令,用线程安全通知队列将结果注入LLM调用前的上下文,实现“边跑边想”。含完整代码、踩坑注释与实操示例,助新手理解AI Agent并发设计核心。
1057 0
|
4月前
|
存储 人工智能 Shell
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(3)TodoWrite (待办写入)
本章详解 s03 版本 TodoWrite 机制:通过 `todo` 工具+`TodoManager` 实现显式任务状态管理(pending/in_progress/completed),强制单任务聚焦;并引入“nag 提醒”——连续3轮未更新待办时自动注入提醒,解决大模型长链路任务健忘问题。代码精简可运行。
1031 3
|
4月前
|
JSON 人工智能 Shell
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(7)Task System (任务系统)
本章详解任务系统(s07):将内存中扁平待办升级为磁盘持久化的有向无环任务图(DAG),支持依赖(blockedBy/blocks)、状态流转(pending→in_progress→completed)与自动解锁,成为多Agent协作的调度骨架。
780 1
|
4月前
|
人工智能 JSON 安全
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(9)Agent Teams (智能体团队)
本章构建持久化智能体团队,解决一次性子智能体和哑后台任务的问题。每个队友拥有独立线程与 LLM 循环,通过 JSONL 文件邮箱实现跨轮通信;TeammateManager 维护团队名册与生命周期,MessageBus 提供线程安全的消息收发,支持并行协作、任务分派与状态同步,为大型项目多 Agent 协同奠定基础。
1396 4
|
4月前
|
人工智能 Shell 开发工具
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(5)Skills (技能加载)
本文介绍AI Agent的技能加载机制,解决系统提示词臃肿问题。通过两层设计:系统提示只保留技能名称(低成本),需要时通过tool_result按需加载完整内容。SkillLoader扫描skills目录下的SKILL.md文件,解析YAML元数据,实现渐进式知识披露,大幅节省token消耗。
2200 1
|
4月前
|
存储 JSON 开发工具
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(12)Worktree + Task Isolation (Worktree 任务隔离)
本章引入 Git worktree 实现任务隔离:为每个任务分配独立目录与分支,通过任务 ID 双向绑定控制平面(`.tasks/`)与执行平面(`.worktrees/`),配合事件总线记录全生命周期,解决多任务并发修改冲突问题,提升可恢复性与可观测性。
772 0
|
4月前
|
人工智能 安全 Shell
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(2)Tool Use (工具使用)
本章详解如何为AI Agent添加专用工具(如read/write/edit_file),通过dispatch map机制实现“加工具只加handler”,无需改动主循环;引入safe_path路径沙箱提升安全性与鲁棒性,告别bash滥用。配套代码清晰易懂,新手友好。
1671 1
|
3月前
|
人工智能 自然语言处理 安全
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
本文介绍了Claude Code终端AI助手的使用指南,主要内容包括:1)常用命令如版本查看、项目启动和更新;2)三种工作模式切换及界面说明;3)核心功能指令速查表,包含初始化、压缩对话、清除历史等操作;4)详细解析了/init、/help、/clear、/compact、/memory等关键命令的使用场景和语法。文章通过丰富的界面截图和场景示例,帮助开发者快速掌握如何通过命令行和交互界面高效使用Claude Code进行项目开发,特别强调了CLAUDE.md文件作为项目知识库的核心作用。
48247 72
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)

热门文章

最新文章