第8章:工具调用与多模态上下文集成

前面四章------采集、检索、记忆、压缩------构成了上下文工程的核心引擎:把正确的信息以正确的密度放进上下文 。但一个AI Agent如果只能"知道"而不能"做到",它的价值就被削去了一半。工具的引入,将上下文工程从一个"输入管理"问题升级为一个"边界管理"问题:上下文不再仅仅是被动接收信息的容器,而是主动连接外部世界的双向接口。本章系统阐述工具作为上下文边界扩展的本质、高质量工具定义的设计方法、工具调用链的上下文管理、以及多模态信息如何与文本上下文统一集成。


8.1 工具调用的本质:扩展AI的上下文边界

工具调用(Tool Calling / Function Calling)是AI Agent与外部世界交互的基本机制。但如果我们只用"让模型调用一个API"的视角来理解工具调用,就大大低估了它在上下文工程中的战略地位。

8.1.1 工具调用 = 动态上下文扩展

从上下文工程的视角看,工具调用的本质是:在推理过程中,通过调用外部函数,动态地将新的信息引入上下文窗口

这个定义有三个关键点:

"在推理过程中"------工具调用不是预处理阶段的数据注入,而是推理过程的有机组成部分。模型在思考的过程中发现"我需要更多信息",于是发起了工具调用。这意味着工具不仅是被动的信息来源,更是模型的主动认知工具。

"动态地"------与第5章的RAG检索不同,工具调用的信息来源不是固定的向量数据库,而是可以执行任意操作的外部系统:查询实时数据库、调用天气API、发送邮件、操作文件系统、执行代码。工具调用将上下文的"信息宇宙"从"预先索引好的文档"扩展到了"整个可计算的世界"。

"引入新的信息"------工具调用的结果(返回值)作为一个新的上下文片段,被注入到当前对话流中。每一组"工具调用 + 工具结果"都是上下文窗口的一次动态增补。

8.1.2 从"被动填充"到"主动感知-行动循环"

在提示工程时代,上下文的构建是单向的:开发者预先写好的提示词 + 用户的输入 → 模型生成回答。工具调用的引入,将这个过程升级为双向的感知-行动循环

scss 复制代码
┌────────────────────────────────────────────────────┐
│              感知-行动循环                          │
│                                                    │
│  ┌──────────┐     ┌──────────┐     ┌──────────┐   │
│  │ 感知上下文 │ ──→ │ 推理决策  │ ──→ │ 工具调用  │   │
│  │ (Context) │     │ (Reason) │     │ (Action) │   │
│  └──────────┘     └──────────┘     └──────────┘   │
│       ↑                                  │         │
│       └────────── 工具结果注入 ──────────┘         │
│                                                    │
│  这不是一次性的输入→输出,而是一个持续循环           │
└────────────────────────────────────────────────────┘

这个循环的本质是:上下文既是模型获取信息的来源,也是模型产生行动的出口。每一次工具调用都是一次"扩圈"------模型从自己当前的认知边界出发,向外探索一步,将探索的结果带回认知边界之内。

8.1.3 工具作为上下文第六维度的"执行出口"

回顾第3章的六维上下文模型,工具定义属于"工具定义层"。但那个模型的静态视角容易让人忽略一个事实:工具定义层不仅是被动的"能力声明"------它同时是上下文与外部世界的桥接层。当模型通过工具调用将上下文中的意图("帮我查一下上周的销售额")转化为外部世界的行动(执行一个SQL查询),并将行动的结果带回上下文,它就在六维模型中的"工具定义层"与"动态状态层"之间建立了一个动态通道。

Karpathy的"LLM=CPU, Context=RAM"类比在这里需要被扩展:如果上下文是RAM,那么工具调用就是CPU的I/O指令------将外部世界的数据读入内存,或将内存中的结果写回外部世界。

8.1.4 工具调用的范式演进

从2023年到2026年,工具调用的范式经历了三个阶段:

阶段 特征 上下文角色
早期ReAct (2023) 一问一答,单步工具调用 工具结果是"一次性的信息补充"
Workflow Agent (2024) 硬约束流程编排,多步工具调用 工具调用链是"上下文中的操作轨迹"
自主Agent (2025-2026) 自主规划,动态发现工具,多工具并行 工具是"模型的认知器官",上下文是感知-行动的环境

进入2026年,工具调用已经从"模型的辅助能力"升级为"模型的认知基础设施"。一个没有工具调用能力的AI,就像一个人被关在没有窗户的房间里------ta可以思考,但无法感知外面的世界,也无法对外面的世界产生任何影响。工具就是那一扇窗户和一双手。


8.2 工具定义的最佳实践

如果把工具调用比喻为一个人使用工具,那么工具定义就是这个工具的"说明书"。一本写得好的说明书让人一看就会用;一本写得差的说明书让人看了更困惑。对于LLM来说,工具定义的质量直接决定了调用的准确性、效率以及错误率。本节从2025-2026年Anthropic、OpenAI和Google的最新指南中抽象出工具定义的通用最佳实践。

8.2.1 Description是灵魂:从"是什么"到"什么时候用"

一个工具定义的质量,80%取决于description字段的质量。但很多开发者在这上面犯的错误是:只写"这个工具是什么"(功能描述),而忽略了"什么时候应该用"(触发条件描述)。

差的description:

arduino 复制代码
"搜索功能"

好的description:

yaml 复制代码
description: >
  在知识库中搜索文档。当用户提到"查找"、"搜索"、"有没有相关文档"、
  "帮我找一下"时使用此工具。返回按相关性排序的文档片段列表。
  注意:此工具只搜索文本内容,不能搜索图片。如果需要搜索图片,请使用 
  search_images 工具。

好的description包含了三个要素:

  1. 功能描述:这个工具做什么("在知识库中搜索文档")
  2. 触发条件:什么情况下应该调用------包含用户可能使用的口语化表达("查找"、"搜索"、"帮我找一下")
  3. 边界说明 :什么情况下不应该调用------以及替代方案是什么("不能搜索图片,请使用search_images")

为什么"什么时候不用"同样重要? 因为LLM在有多个相似工具可选时,决策边界往往是模糊的。同时提供"应该用"和"不应该用"两种信号,可以大幅降低工具调用混淆(回忆第7章7.2.3节的上下文混淆现象------超过30个工具时,工具描述开始重叠混淆)。

8.2.2 参数Schema:用Enum代替自由描述

参数定义的黄金法则是:能用enum约束的值,绝不用自由文本描述

python 复制代码
# 不推荐:参数值靠描述约束
{
    "priority": {
        "type": "string",
        "description": "任务优先级,可选值:critical/high/medium/low"
    }
}

# 推荐:使用enum硬约束
{
    "priority": {
        "type": "string",
        "enum": ["critical", "high", "medium", "low"],
        "description": "任务优先级。critical=系统级故障需立即响应,high=核心功能不可用,medium=部分功能受影响,low=优化类需求"
    }
}

Enum的优势不只是在API层面做了硬约束(模型不可能返回一个不在enum中的值),更重要的是:enum的每个值都可以附带更详细的语义描述 ,帮助模型做出更精准的判断。上面的例子中,如果用户说"这个bug不太紧急,但明天交付前最好修掉",模型看到enum的语义描述后,更可能选择medium而非critical------因为它理解了每个选项的实际含义。

8.2.3 每个参数都需要"专家级"的description

参数的description不只是描述"这是什么",而是描述"如何判断这个值应该是什么"。技术上讲,你写什么样的description,模型就会倾向于提取什么样的信息。两种写法会导致截然不同的调用结果:

python 复制代码
# "这是什么"风格
{
    "due_date": {
        "type": "string",
        "description": "截止日期"  
        # → 模型可能返回 "明天" 或"尽快" 等非标准格式
    }
}

# "如何判断"风格
{
    "due_date": {
        "type": "string",
        "description": "截止日期,格式YYYY-MM-DD。如果用户没有明确指定日期但提到了'紧急',默认为今天;如果用户说'本周内',使用本周五的日期。不得使用相对日期表达(如'明天')"
        # → 模型会返回 "2026-06-12" 这种精确格式
    }
}

8.2.4 Tool Use Examples:传递隐性规则

2025年Anthropic推出的Advanced Tool Use中,最被低估但效果最显著的特性是Tool Use Examples ------在工具定义中附带1-3个真实调用示例。Anthropic官方报告显示,添加示例后工具调用错误率平均降低超过40%

为什么示例如此有效?因为很多工具的使用规则是"隐性"的------用文字描述很冗长,但看一个例子就立即明白。例如:

json 复制代码
{
    "name": "create_ticket",
    "description": "创建系统故障工单",
    "input_schema": {
        "type": "object",
        "properties": {
            "title": {"type": "string", "description": "故障标题,简明扼要"},
            "priority": {
                "type": "string",
                "enum": ["critical", "high", "medium", "low"]
            },
            "labels": {
                "type": "array",
                "items": {"type": "string"},
                "description": "分类标签"
            },
            "due_date": {
                "type": "string",
                "description": "截止日期,格式YYYY-MM-DD"
            }
        },
        "required": ["title"]
    },
    "examples": [
        {
            "scenario": "登录页面返回500错误,生产环境,需要紧急处理",
            "input": {
                "title": "登录页面 500 错误",
                "priority": "critical",
                "labels": ["bug", "authentication", "production"],
                "due_date": "2026-06-12"
            }
        },
        {
            "scenario": "添加暗黑模式UI支持,属于新功能需求,非紧急",
            "input": {
                "title": "支持暗黑模式 UI",
                "priority": "low",
                "labels": ["feature-request", "ui"]
                // 注意:due_date 省略------暗示非紧急功能需求不需要截止日期
            }
        }
    ]
}

两个示例传递了多条隐性规则:

  • 紧急问题(生产环境故障) → "critical" + 今天/明天的due_date + "production"标签
  • 非紧急功能需求 → "low" + 省略due_date + "feature-request"标签
  • title不需要包含"创建"、"新增"等前缀,直接描述问题本身

这些规则如果全用自然语言写出来会很啰嗦,但放在示例里,模型可以类比学习。

8.2.5 结构化错误处理:SERF框架

工具执行可能失败------网络超时、参数错误、权限不足、资源耗尽。如果工具的错误返回只是一条自由文本的错误消息("查询失败,请重试"),模型无法做出确定的错误恢复决策------它只能"猜测"出了什么问题。

2025-2026年,Anthropic的SERF框架(Structured Error Recovery Framework)和学术界提出的CABP协议(Context-Aware Broker Protocol)共同推动了结构化错误处理的标准。核心要求是:工具的错误返回必须包含机器可读的错误分类,而非仅自由文本。

python 复制代码
# 工具错误响应的标准结构
class ToolError:
    """工具错误的标准返回格式"""
    code: str          # 错误分类代码:TRANSIENT / VALIDATION / PERMISSION / RESOURCE / FATAL
    message: str       # 人类可读的错误描述
    details: dict      # 错误详情,包含期望值和实际值
    suggestion: str    # 恢复建议(模型可以直接参考执行)

# 示例:参数校验失败
{
    "error": {
        "code": "VALIDATION",
        "message": "参数 priority 的值 'urgent' 不在允许范围内",
        "details": {
            "expected": ["critical", "high", "medium", "low"],
            "received": "urgent",
        },
        "suggestion": "请从 ['critical', 'high', 'medium', 'low'] 中选择一个值。"
    }
}

# 示例:网络超时
{
    "error": {
        "code": "TRANSIENT",
        "message": "连接超时,目标服务不可达",
        "details": {"timeout_seconds": 30},
        "suggestion": "请等待 5 秒后重试。如果连续 3 次失败,请使用缓存数据并告知用户。"
    }
}

五种错误分类及其处理策略:

错误分类 含义 Agent的确定性处理策略
TRANSIENT 临时性错误(网络超时、服务暂时不可用) 等待后自动重试,指数退避
VALIDATION 参数校验错误(取值不对、格式错误) 根据details修正参数后重试
PERMISSION 权限不足 降级处理,告知用户当前权限限制
RESOURCE 资源耗尽(内存不足、并发限制) 等待或减少请求量后重试
FATAL 不可恢复错误 停止当前执行链,报告给用户

这种结构化设计的意义在于:它将错误处理从"LLM的猜测游戏"变成了"确定性的代码逻辑" 。模型不需要自己推理"这个错误意味着什么"------它可以直接根据code字段执行预先定义好的恢复策略。这不仅降低了错误恢复的延迟,也避免了模型在混乱中做出更错误的决策。

8.2.6 定义工具的三个陷阱

陷阱一:过于宽泛的工具。 一个名为process的工具,description是"处理用户请求"。这种定义让模型在做选择时面临一个模糊的决策边界------什么情况下用process?什么情况下不用?无法判断。好的工具应该职责单一、边界清晰

陷阱二:参数赋予模型过多的解释空间。 如果一个参数的description是"根据情况填写",模型就会开始猜测。如果你想控制模型的输出,就移除所有需要"猜测"的参数,或者给每个参数加上明确的约束。

陷阱三:频繁修改工具定义破坏缓存。 如第7章7.5节所述,工具定义的JSON Schema是KV缓存前缀的组成部分。每次修改工具定义------哪怕只是加一个参数、改一个description------都会导致整个缓存前缀失效。工具定义应该被当作"基础设施代码"来对待:设计时深思熟虑,上线后尽可能稳定。


8.3 工具调用的上下文管理

工具调用本身会产生大量上下文------一次搜索可能返回5000token的结果,一次文件读取可能加载整个文件内容,一次数据库查询可能拉取上千行数据。如果不加管理,这些工具结果会迅速填满上下文窗口,淹没真正重要的信息。本节讨论工具结果在上下文中的过滤、压缩和格式化策略。

8.3.1 工具结果的生命周期管理

被工具调用产生的结果,在上下文中有一个"生命周期":

复制代码
工具调用 → 结果注入上下文 → 模型消费结果 → 结果失去活性 → 清除或压缩

这个生命周期的关键节点是"模型消费结果"------一旦模型已经读取并推理过工具结果,其结果对后续推理的边际价值就急剧下降。这正是第7章7.4.1节讨论的Anthropic Tool Result Clearing的核心依据。

python 复制代码
# 工具结果的生命周期管理
class ToolResultLifecycle:
    def __init__(self):
        self.results = {}   # {tool_call_id: ToolResult}
        self.consumed = set()  # 已被模型消费的tool_call_id集合
    
    def add_result(self, tool_call_id: str, result: dict):
        """工具调用返回结果后,注入上下文"""
        self.results[tool_call_id] = {
            "data": result,
            "timestamp": time.time(),
            "tokens": estimate_tokens(result),
            "status": "active"  # active → consumed → compressed
        }
    
    def mark_consumed(self, tool_call_id: str):
        """模型已读取并推理过该结果"""
        if tool_call_id in self.results:
            self.results[tool_call_id]["status"] = "consumed"
            self.consumed.add(tool_call_id)
    
    def compress_or_clear(self, tool_call_id: str) -> dict:
        """对已消费的结果进行压缩或清除"""
        result = self.results[tool_call_id]
        
        # 对于可重建的结果(可以重新执行查询获得),直接清除
        if self._is_reconstructible(tool_call_id):
            return {"status": "cleared", "note": f"[工具结果已清除: {tool_call_id}]"}
        
        # 对于不可重建的结果(如用户当时的状态),压缩保留
        summary = self._generate_compact_summary(result["data"])
        return {"status": "compressed", "summary": summary}

8.3.2 工具结果的过滤策略

并非每个工具返回的所有信息都需要进入上下文。以下是三种经过产线验证的过滤策略:

策略一:基于Token阈值的自动截断。 这是最基础也最不可少的策略。如果工具返回的结果超过某个token阈值(如2000 tokens),自动截断并附加提示。

python 复制代码
def auto_truncate(result: str, max_tokens: int = 2000) -> str:
    """超过阈值时自动截断工具结果"""
    tokens = count_tokens(result)
    if tokens <= max_tokens:
        return result
    
    truncated = truncate_to_tokens(result, max_tokens)
    return f"{truncated}\n\n[结果已截断:原始长度为 {tokens} tokens。"
    f"如需完整内容,请使用 get_full_result(tool_call_id='xxx') 获取]"

策略二:字段级过滤。 对于结构化输出(JSON、CSV),只保留与当前任务相关的字段,移除无关字段。例如,数据库查询返回了30列数据,但当前任务只需要其中5列------其余25列不应该进入上下文。

python 复制代码
def field_level_filter(result: dict, relevant_fields: list[str]) -> dict:
    """只保留相关字段,减少上下文噪声"""
    return {
        key: value 
        for key, value in result.items() 
        if key in relevant_fields
    }

策略三:摘要代替全文。 对于长文档/长列表返回,用一个小模型先对结果做摘要,将摘要注入上下文,同时保留原始数据的引用路径。这是第7章7.3节"紧凑化"策略在工具结果管理中的具体应用。

8.3.3 工具结果的格式化:让模型快速"理解"

工具结果应该在进入上下文之前被格式化成模型最容易消费的形式。两条核心原则:

原则一:结构化优先。 JSON / Markdown表格 > 自由文本。模型对结构化信息的提取效率远高于自由文本。如果工具返回的是自由文本,考虑在注入上下文前做一次结构化转换。

原则二:内容类型显式标注。 在工具结果前明确标注这是什么类型的信息,帮助模型快速建立认知框架。

python 复制代码
def format_tool_result(tool_name: str, result: dict) -> str:
    """将工具结果格式化为模型友好的上下文"""
    if tool_name == "search_documents":
        # 搜索结果 → Markdown 列表
        items = result.get("items", [])
        lines = [f"📄 **搜索 '{result['query']}' 的结果**(共 {len(items)} 条,按相关性排序):\n"]
        for i, item in enumerate(items[:5], 1):
            lines.append(f"{i}. **{item['title']}** (相关性: {item['score']:.2f})")
            lines.append(f"   {item['snippet'][:200]}")
            lines.append(f"   → 来源: `{item['source']}`")
        return "\n".join(lines)
    
    elif tool_name == "execute_sql":
        # SQL结果 → Markdown 表格
        columns = result.get("columns", [])
        rows = result.get("rows", [])
        header = "| " + " | ".join(columns) + " |"
        separator = "|" + "|".join(["---" for _ in columns]) + "|"
        data_rows = ["| " + " | ".join(str(c) for c in row) + " |" for row in rows[:20]]
        return f"📊 **SQL查询结果**({len(rows)} 行, {len(columns)} 列):\n\n{header}\n{separator}\n" + "\n".join(data_rows)
    
    else:
        # 通用格式
        return json.dumps(result, ensure_ascii=False, indent=2)

8.4 渐进披露模式

渐进披露(Progressive Disclosure)是上下文工程中最重要的架构模式之一。它的核心思想来自UI设计领域------不要在首页展示所有设置项,而是根据用户的探索路径逐步展现。应用到Agent的工具管理上:不要一次性将所有工具定义塞进上下文,而是让Agent在需要时按需发现和加载

8.4.1 三层加载架构

渐进披露将工具信息分为三个层级,逐级加载:

yaml 复制代码
┌──────────────────────────────────────────────────┐
│  Layer 1: 发现层 (Discovery) ------始终在上下文       │
│  ─────────────────────────────────               │
│  工具名称 + 一句话描述(~15 tokens/工具)           │
│  50个工具 = ~750 tokens                          │
│                                                  │
│  作用:让模型知道"有哪些工具可用"                  │
│  加载时机:Agent启动/每个对话会话开始时             │
├──────────────────────────────────────────────────┤
│  Layer 2: 激活层 (Activation) ------按需加载          │
│  ────────────────────────────────                │
│  完整参数Schema + 约束说明(~400 tokens/工具)      │
│  只有被选中或与当前任务高度相关的工具才加载          │
│                                                  │
│  作用:让模型知道"如何准确地使用这个工具"           │
│  加载时机:工具被匹配或Agent判断需要该工具时         │
├──────────────────────────────────────────────────┤
│  Layer 3: 执行层 (Execution) ------按需加载           │
│  ──────────────────────────────                  │
│  Few-shot示例 + 参考文档(~1000 tokens/工具)      │
│  只在工具需要被实际调用时加载                       │
│                                                  │
│  作用:传递隐性规则和复杂使用模式                   │
│  加载时机:工具即将被调用,且当前任务的复杂度需要示例  │
└──────────────────────────────────────────────────┘

这个三层架构的核心价值在于:上下文从"所有工具的完整定义"变成了"当前任务所需工具的精准信息" 。拥有100个工具的Agent,不再需要从100×500=50000 tokens起步,而是从800 tokens(发现层)起步,每个实际用到的工具额外增加400-1500 tokens。

8.4.2 Manus的20个核心原子工具实践

Manus团队在实践中验证了一条重要经验:将工具数量控制在约20个核心原子工具,复杂逻辑通过代码和包来实现。Manus联合创始人季逸超(Peak Ji)的原话是"We cap the tool count at roughly twenty core atomic tools. Complex logic chains are offloaded to code and packages."

这个设计背后的逻辑是三个层次的协同:

Layer 1 --- 核心原子工具(~20个,始终在上下文): 文件读写、浏览器导航、搜索、代码执行、Shell命令等------任何Agent任务都需要的基础能力。这些工具的定义token消耗是固定的(约1500 tokens),不随Agent功能增加而膨胀。

Layer 2 --- 沙箱工具(通过bash CLI间接暴露): 数据库操作、网络请求、数据分析、包管理------这些功能不通过独立的工具定义暴露,而是通过Layer 1中的execute_command工具间接调用。例如,Agent不是调用一个名叫query_database的工具,而是在Shell中执行python3 query_db.py。这意味着Layer 2的功能可以无限扩展,但上下文的工具定义token丝毫不增。

Layer 3 --- 复杂逻辑通过代码实现: Agent不是调用一个预定义的"分析销售数据"工具,而是自己编写一个Python脚本来分析销售数据,然后通过Layer 1的Shell执行。这让Agent的能力不受预定义工具的限制------Layer 1永远只有20个工具,但Agent可以通过编程组合出无限种操作。

yaml 复制代码
┌─────────────────────────────────────────────────────┐
│              Manus 三层行动空间                       │
│                                                     │
│  Layer 1: 核心原子工具 (~20个)                        │
│  ┌───────────────────────────────────────────────┐  │
│  │ browser_navigate  browser_click  browser_type  │  │
│  │ shell_exec  file_read  file_write  search      │  │
│  │ → 始终在上下文 (~1500 tokens 固定)              │  │
│  └───────────────────────────────────────────────┘  │
│                          │                          │
│  Layer 2: 沙箱能力(通过 shell_exec 间接暴露)        │
│  ┌───────────────────────────────────────────────┐  │
│  │ pip install pandas / npm install / curl ...    │  │
│  │ → 不占工具定义 token,Agent 直接 shell 调用      │  │
│  └───────────────────────────────────────────────┘  │
│                          │                          │
│  Layer 3: 复合逻辑(Agent 编写代码实现)              │
│  ┌───────────────────────────────────────────────┐  │
│  │ 数据分析脚本 / 文件批处理 / API 组合调用          │  │
│  │ → 不受预定义限制,Agent 通过编程扩展能力          │  │
│  └───────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────┘

这种设计带来的工程收益是双重的:上下文中的工具定义永不膨胀 (缓存命中率稳定),Agent的能力边界几乎是无限的(因为代码可以做任何事)。

当工具数量超过50个时,即便是"发现层"(仅名称和一句话描述,~750 tokens)也可能让模型眼花缭乱。Anthropic在2025年推出的Tool Search Tool------本质上是一个"工具的搜索引擎"------解决了这个问题。

工作方式:

  1. 不在上下文中预加载任何工具定义
  2. 模型如果觉得需要某个工具,调用Tool Search Tool,用自然语言描述需求
  3. Tool Search Tool返回最匹配的2-5个工具的发现层信息
  4. 模型从中选择,激活层和执行层按需加载

Anthropic的测试数据显示:在50+工具的场景下,Tool Search Tool将初始上下文从约77,000 tokens降至约8,700 tokens(减少约90%),同时将工具选择准确率从49%提升至74%。

8.4.4 Claude Code的Skills系统

Claude Code的Skills系统是渐进披露模式的另一个典型实现。Skills以Markdown文件的形式存储在工作区的.claude/skills/目录中,通过文件系统实现"按需发现和加载":

bash 复制代码
.claude/skills/
├── SKILL.md              # 主技能定义
├── chart-visualization/
│   ├── SKILL.md           # YAML frontmatter 包含 description(发现层)
│   ├── references/        # 按需加载的文档(执行层)
│   │   ├── bar_charts.md
│   │   └── line_charts.md
│   ├── examples/          # 输入输出示例
│   └── scripts/           # 可执行脚本
│       └── generate_chart.py
└── database-query/
    ├── SKILL.md
    └── references/
        ├── postgres.md
        └── mysql.md

工作流程:

  1. Agent启动时,上下文仅包含基础工具(bash, read, edit)和系统指令(~800 tokens)
  2. Agent通过ls .claude/skills/列出所有Skill的名称和description(仅YAML frontmatter,每个~50 tokens)
  3. 当用户任务匹配某个Skill的触发条件时,Agent通过cat .claude/skills/xxx/SKILL.md读取完整指令
  4. 如果SKILL.md中的工作流引导到了references/中的文档,Agent按需加载

一个编写良好的SKILL.md的关键是YAML frontmatter中的description------它不仅是Skill的"介绍",更是Agent判断"是否使用这个Skill"的唯一依据:

yaml 复制代码
---
name: chart-visualization
description: >
  This Skill should be used when the user asks to create charts, 
  visualize data, generate graphs, or plot statistics. Not for 
  creating diagrams (use diagram-skill instead).
allowed-tools:
  - Read
  - Grep
  - Bash(python:*)
---

8.5 "Just-in-Time"上下文策略

如果说渐进披露是关于"什么时候加载什么工具",那么Just-in-Time(JIT,即时)上下文就是关于"什么时候加载什么信息"。两者的逻辑是相同的------信息应该在需要的时刻被引入上下文,而不是在一切开始之前就被预装填满。

8.5.1 Anthropic的JIT哲学

Anthropic在《Effective Context Engineering for AI Agents》中给出的核心理念是:维护轻量级标识符(文件路径、查询链接、元数据摘要),运行时通过工具动态加载完整数据。Agent在上下文的最初状态是"瘦"的------只有定位信息,没有具体内容。随着Agent在任务中的探索和推理,"需要什么就去拿什么"。

这个哲学与传统的"全量预加载"形成了鲜明对比:

维度 全量预加载 Just-in-Time
初始上下文大小 大(可能数万token) 小(~800 token)
上下文中的噪声 高(大量无关信息) 低(只有当前需要的信息)
灵活性 低(预加载的内容是静态的) 高(根据任务动态获取)
延迟 无检索延迟 每次按需加载增加一次工具调用延迟
缓存友好度 高(前缀完全固定) 中(需要管理工具调用的增量)

Just-in-Time不是零成本的------每次按需加载都需要一次工具调用(如cat a-file.md),增加了交互轮次和延迟。但正如第7章7.1节的数据所示,上下文增长带来的延迟惩罚是非线性的(O(n²)注意力) ,而按需加载的工具调用延迟是线性的。在长上下文场景下,JIT的整体延迟通常低于全量预加载。

8.5.2 Claude Code的混合模式

Claude Code在实践中并没有采用"纯JIT"或"纯预加载"的极端方案,而是走了一条混合路线:

  • 预加载部分CLAUDE.md文件------项目的全局配置和规则------在每次新会话中自动加载。这相当于Agent的"长期记忆"------稳定、精简、跨任务共用。
  • JIT部分 :项目中除CLAUDE.md外的所有其他文件,根据任务需要通过glob/grep/read按需加载。Agent不是先读一遍整个项目再做操作,而是"在操作中自然发现哪些文件相关"。

这种混合模式的聪明之处在于:它模拟了人类工程师的工作方式------你不会在开始编码前把整个代码库读一遍,但你会看一眼README和项目结构,然后在编码过程中自然地打开和关闭相关文件。

8.5.3 JIT的工程实现模式

python 复制代码
class JustInTimeContextManager:
    """JIT上下文管理器------按需加载,而非预填满"""
    
    def __init__(self, llm, file_system):
        self.llm = llm
        self.fs = file_system
        self.loaded_resources = {}  # 已加载的资源,避免重复加载
    
    async def build_initial_context(self, project_path: str) -> dict:
        """构建最精简的初始上下文"""
        # 只加载索引性的元数据,不加载具体内容
        overview = {
            "project_structure": await self.fs.ls(project_path, max_depth=2),
            "config_file": await self._find_config(project_path),
            "available_skills": await self._list_skills(project_path),
        }
        return {"role": "system", "content": self._format_overview(overview)}
    
    async def resolve_resource(self, reference: str) -> str:
        """按需解析资源引用------加载完整内容"""
        if reference in self.loaded_resources:
            return self.loaded_resources[reference]
        
        content = await self.fs.read(reference)
        self.loaded_resources[reference] = content
        return content
    
    async def should_unload(self) -> list[str]:
        """判断哪些已加载的资源可以卸载"""
        # 根据资源大小和最后使用时间决定
        to_unload = []
        for ref, (content, last_used) in self.loaded_resources.items():
            if time.time() - last_used > 300:  # 5分钟未使用
                to_unload.append(ref)
        for ref in to_unload:
            del self.loaded_resources[ref]
        return to_unload

8.6 多模态上下文集成

到目前为止,本书讨论的上下文都是"文本上下文"------文字的排列组合。但2026年的现实是:AI Agent需要处理的不再只是文本。用户会上传截图、录制语音、分享视频片段。上下文工程必须回答一个基本问题:如何将图像、音频、视频与文本统一集成到同一个上下文窗口中?

8.6.1 2026年多模态统一表示的技术现状

2026年的前沿模型(GPT-4o、Claude 4、Gemini 2.5 Pro)都已经原生支持多模态输入。它们在底层采用了一个统一的技术思路:将所有模态的输入转化为同一个向量空间中的token序列

  • 图像:被切分为固定大小的patch(如16×16像素),每个patch通过视觉编码器转化为一个embedding向量。这些embedding与文本token的embedding存在于同一个向量空间中。一张512×512的图片大约被转化为256-1024个视觉token。
  • 音频:通过Whisper风格的编码器,将音频波形转化为连续的音频token序列。1分钟的音频大约产生1500-3000个音频token。
  • 视频:被拆解为关键帧序列(每秒1-2帧)加上对应的音频流,分别按图像和音频的方式编码,最后拼合成一个统一的token序列。

当前主流模型的上下文窗口能力对比(2026年Q2):

模型 最大上下文窗口 图像支持 音频支持 视频支持
Gemini 2.5 Pro 100万 token 原生 原生 原生(可达1小时)
GPT-4o 12.8万 token 原生 原生 有限(关键帧方案)
Claude Opus 4.6 20万 token 原生 有限 有限

Gemini 2.5 Pro在上下文窗口方面的领先优势在2026年依然明显------100万token的理论窗口意味着它可以一次性处理约1小时的视频或数十张高分辨率图片。但实际的有效窗口(模型能始终保持高注意力的部分)约为75万token。

8.6.2 多模态上下文的"三层表示"策略

将多模态信息直接以raw形式(图像像素、音频波形)放入上下文虽然可行,但并不总是最优的。因为:

  1. 视觉/音频token比文本token贵。一张图片的视觉token(256-1024个)的推理成本远高于同量的文本token
  2. 模型对"看"的理解可能不如对"读"的理解精准。给模型一张复杂表格的截图,不如直接给模型markdown格式的表格数据
  3. 视觉token无法被后续的文本推理步骤"引用"。模型可以引用"第3行第2列的值是50万",但不能精确引用"图片右上角的那个数字是50万"

因此,2026年的最佳实践是三层表示

swift 复制代码
┌─────────────────────────────────────────────────────┐
│          多模态上下文的三层表示                        │
│                                                     │
│  Layer 1: 文本描述(始终在上下文)                     │
│  ───────────────────────────────                    │
│  由预处理器生成的文字描述:                            │
│  · "图片显示了一个登录页面,包含用户名和密码输入框"      │
│  · "音频内容是关于Q2销售数据的讨论,提到了..."          │
│  · 表格的结构化文本提取                               │
│  → 优势:成本低、可被检索、可被后续推理引用             │
│                                                     │
│  Layer 2: 原始数据引用(需要时恢复)                    │
│  ───────────────────────────────                    │
│  文件路径 + 元数据:                                 │
│  · "原始截图: ./screenshots/login-error.png"        │
│  · "原始音频: ./recordings/meeting-q2.wav (12分钟)"  │
│  → 优势:需要细节分析时可重新加载,不占用上下文          │
│                                                     │
│  Layer 3: 模型原生感知(选择性启用)                    │
│  ─────────────────────────────────                  │
│  仅对确实需要模型"看"或"听"的内容启用:                 │
│  · 需要识别图片中的UI细节、颜色、布局                   │
│  · 需要理解语音中的情感和语气                          │
│  → 优势:直接感知,无需中间文本描述的信息损失            │
└─────────────────────────────────────────────────────┘

三层策略的决策树:

swift 复制代码
收到多模态输入
    │
    ├── 是否可以用文本精确描述?(如表格、图表数据)
    │       └── 是 → Layer 1 文本描述为主 + Layer 2 引用备份
    │
    ├── 是否需要精确的视觉/听觉细节?(如UI截图的像素级分析)
    │       └── 是 → Layer 3 原生感知 + Layer 1 摘要
    │
    ├── 是否需要反复访问?
    │       └── 是 → Layer 2 引用在上下文中,按需展开
    │
    └── 一次性的快速问题?(如"这张图里有什么")
            └── Layer 3 原生感知,用完即弃

8.6.3 实践:将截图转化为Agent可操作的信息

以Agent最常见的多模态场景------用户上传一张错误截图------为例,展示三层策略的具体运作:

python 复制代码
async def process_user_screenshot(image_path: str, user_query: str) -> dict:
    """处理用户上传的截图"""
    
    # Layer 1: 生成文本描述(始终注入上下文)
    description = await vision_model.describe(image_path, focus=[
        "页面元素和布局(是什么页面)",
        "错误信息和错误码",
        "当前的操作状态(卡在哪一步)",
        "URL路径或路由信息",
        "任何可见的日志或调试信息"
    ])
    
    # Layer 2: 保存原始数据引用
    reference = {
        "path": image_path,
        "resolution": get_image_resolution(image_path),
        "size": os.path.getsize(image_path),
        "timestamp": time.time()
    }
    
    # Layer 3: 如果用户查询需要细节分析,启用原生感知
    if needs_visual_detail(user_query):
        raw_visual = {
            "type": "image",
            "source": encode_image_base64(image_path),
            "detail": "high"
        }
    else:
        raw_visual = None
    
    return {
        "description": description,
        "reference": reference,
        "raw_visual": raw_visual,  # None 或 base64
        "context_tokens_used": estimate_tokens(description)
    }

8.6.4 视频和音频的预处理策略

音频处理:

2026年的主流方案是:

  1. 使用Whisper(开源)或Gemini原生接口将音频转写为带时间戳的文本(转写成本:Whisper API约$0.006/分钟)
  2. 如果涉及情感分析或语气判断,保留原始音频的文件引用,需要时让模型原生感知
  3. 将转写文本作为上下文的主体,音频文件路径作为备份引用
python 复制代码
async def preprocess_audio(audio_path: str) -> dict:
    """音频预处理------转写+情感检测"""
    # Step 1: 转写为文本
    transcription = await whisper_api.transcribe(
        audio_path, 
        language="zh",
        response_format="verbose_json"  # 带时间戳
    )
    
    # Step 2: 生成摘要和关键片段的时间戳
    segments = transcription.get("segments", [])
    key_moments = extract_key_moments(segments)
    
    return {
        "transcription_summary": summarize_transcription(transcription["text"]),
        "duration_seconds": transcription.get("duration", 0),
        "key_moments": key_moments,  # [{start: 12.5, description: "提到关键问题"}]
        "full_transcription_ref": f"./transcripts/{os.path.basename(audio_path)}.txt",
        "raw_audio_ref": audio_path  # 需要情感分析时使用
    }

视频处理:

视频是最高成本的多模态处理对象。目前没有任何模型可以直接将完整视频作为token序列高效处理。2026年的工程方案是:

  1. 使用FFmpeg抽取关键帧(每秒1-2帧或按场景变化检测)
  2. 提取音频轨道进行语音转写
  3. 生成"关键帧描述 + 音频转写文本 + 时间轴对齐"的结构化表示

对于大多数Agent任务来说,这个结构化表示已经足够支持推理和决策。只有在用户明确要求"分析这个视频的视觉效果/画面变化"时,才将关键帧以原生视觉token的形式送入模型。


8.7 工具调用链的上下文跟踪

当一个Agent需要执行多步操作时------"先搜索A,然后根据A的结果查询B,根据B的结果分析C"------每一步的中间结果都会在上下文中留下痕迹。这些痕迹如果不加管理,会在上下文中积累成一团乱麻:哪些结果已经被消费了?哪些中间状态还需要保留?如果第3步失败了,能否回溯到第2步而不是从头开始?

工具调用链的上下文跟踪要解决的就是这个"多步操作的可追溯性和可恢复性"问题。

8.7.1 为什么需要显式跟踪

在简单的单步工具调用场景下,跟踪不是问题------工具调用→工具结果→模型消费→任务完成。但在多步Agent场景下,如果没有显式的跟踪机制,就会出现以下问题:

  • 迷路问题:Agent执行了8步操作后,无法快速回顾"这一切是从哪里开始的",上下文中的信息散落在各个轮次中,检索成本高。
  • 分歧问题:Agent在第5步做了一个决策,在第8步基于这个决策又做了另一个决策------如果第5步的决策是错误的,Agent无法自动识别"前面的决策导致了一系列错误"。
  • 回滚问题:Agent在第6步失败了,但上下文中已经混杂了前面5步的完整执行痕迹。Agent无法确定"应该从哪一步重新开始"。

这些问题的共同根源是:工具调用的中间结果被"平铺"在了上下文的时间序列中,缺乏一张"地图"来导航这些结果之间的关系

8.7.2 文件系统追踪:Agent的"操作日志"

在2026年的Agent实践中,最常用也最务实的追踪方式是文件系统追踪------Agent将自己的每一步操作记录到一个结构化的trace文件中。这个方案的优雅之处在于:它不需要额外的数据库或基础设施,文件系统本身就是最可靠的追踪存储。而且agent天生就具备读写文件的能力,不需要引入新的工具。

python 复制代码
class FileSystemTrace:
    """基于文件系统的工具调用链追踪"""
    
    def __init__(self, workspace: str, session_id: str):
        self.trace_dir = f"{workspace}/.agent/traces/{session_id}"
        os.makedirs(self.trace_dir, exist_ok=True)
        self.trace_file = f"{self.trace_dir}/trace.md"
        self.checkpoint_dir = f"{self.trace_dir}/checkpoints"
        os.makedirs(self.checkpoint_dir, exist_ok=True)
    
    def record_step(self, step_num: int, tool_name: str, 
                    params: dict, result_summary: str, 
                    decision: str = None):
        """记录一个工具调用步骤"""
        timestamp = datetime.now().isoformat()
        
        entry = f"""
### 步骤 {step_num} ({timestamp})
- **工具**: `{tool_name}`
- **参数**: `{json.dumps(params, ensure_ascii=False)}`
- **结果摘要**: {result_summary}
{"- **决策**: " + decision if decision else ""}
- **完整结果**: `./checkpoints/step_{step_num}_result.json`
"""
        # 追加到trace文件
        with open(self.trace_file, 'a') as f:
            f.write(entry)
        
        # 保存完整结果到checkpoint
        with open(f"{self.checkpoint_dir}/step_{step_num}_result.json", 'w') as f:
            json.dump({"timestamp": timestamp, "tool": tool_name,
                       "params": params, "result_summary": result_summary}, 
                      f, ensure_ascii=False, indent=2)
    
    def get_recent_steps(self, n: int = 5) -> str:
        """获取最近N步的追踪摘要------注入上下文用"""
        if not os.path.exists(self.trace_file):
            return "[尚无追踪记录]"
        
        with open(self.trace_file, 'r') as f:
            content = f.read()
        
        # 提取最近N个步骤
        steps = content.split("### 步骤 ")
        recent = steps[-n:] if len(steps) > n else steps[1:]
        
        return "## 最近操作追踪\n\n" + "\n".join(
            f"### 步骤 {s}" for s in recent
        )
    
    def create_checkpoint(self, label: str, context_snapshot: dict):
        """创建完整的状态检查点------用于回滚"""
        checkpoint = {
            "label": label,
            "timestamp": datetime.now().isoformat(),
            "context": context_snapshot,
            "trace": open(self.trace_file).read() if os.path.exists(self.trace_file) else ""
        }
        safe_label = label.replace(" ", "_").replace("/", "-")
        with open(f"{self.checkpoint_dir}/checkpoint_{safe_label}.json", 'w') as f:
            json.dump(checkpoint, f, ensure_ascii=False, indent=2)

对于一个Agent来说,trace.md不仅是调试工具------它本身就是上下文的一部分 。在长周期任务中,Agent可以定期读取自己的trace.md来回顾"我做了哪些步骤、当前的进度如何、还有哪些待完成"。这是一种"外部化的工作记忆"------类似人类在解决复杂问题时在纸上列出的步骤清单。

8.7.3 LangGraph的持久化状态与时间旅行

比文件系统更强大的追踪方案是框架级的持久化状态管理。LangGraph的Checkpointer机制是其中的代表------它能在每一步执行后自动保存完整的状态快照,支持"时间旅行"式的回滚和重放。

python 复制代码
from langgraph.graph import StateGraph
from langgraph.checkpoint.sqlite import SqliteCheckpointer
import sqlite3

# 定义Agent的状态
class AgentState(TypedDict):
    messages: list
    current_step: int
    completed_steps: list
    tools_used: list

# 创建图
graph = StateGraph(AgentState)
# ... 定义节点和边 ...

# 使用SQLite作为检查点存储
conn = sqlite3.connect("agent_checkpoints.db")
checkpointer = SqliteCheckpointer(conn)

# 编译时传入检查点
app = graph.compile(checkpointer=checkpointer)

# 每次执行自动保存状态
result = app.invoke(
    initial_state,
    config={"configurable": {"thread_id": "task-001"}}
)

# 查看历史状态------时间旅行!
history = list(app.get_state_history(
    config={"configurable": {"thread_id": "task-001"}}
))

# 从第5步的状态重新执行(跳过第5步之后的所有操作)
app.invoke(
    None,  # 不传入新状态,使用checkpoint中的状态
    config={
        "configurable": {
            "thread_id": "task-001",
            "checkpoint_id": history[5].config["configurable"]["checkpoint_id"]
        }
    }
)

LangGraph的Checkpointer机制解决了文件系统追踪无法优雅处理的一个问题:精确回滚。在文件系统方案中,回滚需要Agent手动判断"从哪一步重新开始"并恢复该步骤的状态。而在状态机方案中,回滚是框架级的------直接跳转到指定的checkpoint,就像程序调试中的"回到断点"。

8.7.4 工具调用链追踪的三条设计原则

原则一:不可变历史。 每一步操作的记录都是追加的(append-only),不会被修改或删除。这不仅是为了调试------更重要的是,它防止了7.2.1节描述的"上下文中毒"效应在追踪层发生。如果追踪被错误地修改了,Agent基于错误的追踪做出的决策就会连锁错误。

原则二:因果链接。 每一步记录不仅要记录"做了什么",还要记录"为什么做"------这一步是基于前面哪一步的什么结果做出的决策。这可以通过在追踪记录中显式声明依赖关系来实现:

python 复制代码
def record_step_with_dependency(step_num, tool_name, params, 
                                 depends_on: list[int], reason: str):
    """记录带因果链接的步骤"""
    # 明确标注这一步依赖于哪些前序步骤
    dependency_info = f"依赖步骤: {depends_on}, 原因: {reason}"
    # ...

原则三:检查点粒度与成本平衡。 不是每一步都需要创建完整的状态快照(成本高)。只在"关键节点"创建checkpoint------任务阶段性完成、做出重要决策、检测到异常情况时。一般任务每5-10步创建一次checkpoint即可。


本章小结

本章从工具调用的本质出发,系统性地构建了工具调用与多模态上下文集成的完整工程方法论。

8.1节 重新定义了工具调用的本质------它不仅仅是"调用API",而是将上下文从一个"被动的信息容器"升级为一个"主动的感知-行动循环"。工具调用是模型认知边界的扩展机制------将"可计算的世界"连接到上下文中。

8.2节 详细阐述了工具定义的六大最佳实践:description必须同时包含功能描述和触发条件、用enum替代自由文本描述、每个参数需要"专家级"的description、Tool Use Examples传递隐性规则(错误率降低40%以上)、SERF结构化错误处理框架(五种错误分类与确定性恢复策略)、以及"职责单一、边界清晰、保持稳定"三个定义陷阱。

8.3节 讨论了工具调用的上下文管理------工具结果的生命周期(注入→消费→清除/压缩),三种过滤策略(token阈值截断、字段级过滤、摘要代替全文),以及两条格式化原则(结构化优先、内容类型显式标注)。

8.4节 深入渐进披露模式------三层加载架构(发现层/激活层/执行层),Manus的20个核心原子工具实践(上下文token固定,能力无限扩展),Anthropic的Tool Search Tool(50+工具场景下初始上下文减少90%,准确率提升至74%),以及Claude Code的Skills文件系统实现。

8.5节 分析了Just-in-Time上下文策略------从"全量预加载"到"按需获取"的范式转变,Claude Code的混合模式(预加载CLAUDE.md + JIT其他文件),以及JIT的工程实现模式。

8.6节 探讨了多模态上下文集成的三层表示策略(文本描述/原始引用/原生感知)------解决了"应该用文本描述还是原生视觉/音频"的决策问题,并给出了视频和音频的预处理工程方案。

8.7节 讨论了工具调用链的上下文跟踪------文件系统追踪(Agent的"操作日志")、LangGraph的状态机Checkpointer(时间旅行和精确回滚),以及不可变历史、因果链接、检查点粒度三条设计原则。

如果说第5章是上下文"读"的能力(从外部知识库检索信息),第6章是上下文"记忆"的能力(跨越时间的信息持久化),第7章是上下文"优化"的能力(保持精瘦而有信号),那么本章就是上下文 "行动与感知" 的能力------让AI不仅仅是信息的消费者,更是世界的参与者。在下一章,我们将进一步扩展这个视角:当多个Agent同时运作时,上下文如何在它们之间共享、隔离和协调?

相关推荐
葡萄城技术团队1 小时前
三大适配场景:释放AI Coding真实落地价值(四)
人工智能
MomentYY1 小时前
RAG 混合检索:关键词 + 语义
人工智能·agent·ai编程
Black蜡笔小新1 小时前
EasyAIS+国标GB28181视频监控平台EasyCVR强强联动,全域视频AI识别能力落地!
大数据·人工智能·音视频
Microvision维视智造1 小时前
智能工厂等级自检表:梯度培育,你的工厂在第几级?
人工智能·计算机视觉·视觉检测·机器视觉
时光不负努力1 小时前
skill 定义 + 多个skill 协作
人工智能·openai
武子康2 小时前
MCP 2026-07-28 无状态核心之后:身份、任务、幂等与审计状态到底放在哪里?
人工智能·llm·mcp
瓦学妹2 小时前
X(Twitter)新号如何防封?2026 养号与防限流全攻略
大数据·网络·人工智能·新媒体运营·twitter
EAIReport2 小时前
企业GEO全域运营技术落地路线与量化效果验证方案(AI大数据行业适配)
大数据·人工智能
极地野狼 音乐哔哔2 小时前
AI Agent 30天速成|Day4 笔记
人工智能·笔记