第七章:模型集成与输出治理¶
模型集成是智能体系统与 LLM 的边界。设计不当的集成会导致:
- 幻觉(输出不真实信息)
- 性能问题(过度推理、Token 浪费)
- 安全隐患(输出不可控)
- 兼容性问题(难以支持多个模型)
本章研究如何设计一个健壮、灵活、高效的模型集成层,包括多模型切换、输出解析、质量门控、幻觉检测等机制。
核心设计问题
- 模型抽象:如何设计统一接口支持多个模型(Claude、GPT、DeepSeek 等)?
- 输出解析:如何从原始 API 响应中安全地提取和验证结构化输出?
- 质量保证:在工具调用执行前如何验证输出的合法性?
- 幻觉检测:如何识别和防止智能体调用不存在的工具或生成错误的参数?
参考系统的方法
- OpenClaw:支持 Claude/GPT/DeepSeek/Gemini/Ollama,带模型回退链路
- Claude Code:绑定 Claude,专注于输出质量和推理预算管理
1. 模型抽象层设计¶
模型抽象层连接智能体应用与具体 LLM API,支持灵活的多模型或单模型策略。本节介绍配置管理系统、故障转移链路、Provider 接口设计和模型选择引擎。
1.1 核心概念¶
模型抽象层是连接智能体应用与具体 LLM API 的中间层。其目标是:
- 统一接口:提供一致的 API,支持 Claude、GPT、DeepSeek、Gemini 等多种模型
- 灵活切换:动态选择或回退模型,无需修改业务逻辑
- 配置驱动:通过配置文件管理模型选择、默认值、故障转移策略
- 错误隔离:单个模型的故障不影响整个系统
1.2 设计权衡:多模型 vs 单模型绑定¶
Claude Code 方案(单模型绑定)
- 绑定 Claude 模型家族(Opus/Sonnet/Haiku)
- 专注于版本管理与推理预算调优
- 深度集成 Adaptive Thinking 等 Claude 特性
- 场景:需要最佳性能、深度功能集成、模型特性充分利用
OpenClaw 方案(多模型支持)
- 支持多供应商切换(Claude/GPT/DeepSeek/Gemini/Ollama)
- 供应商认证与接入配置(openclaw.json)
- 模型故障转移链路(fallback mechanism)
- 场景:需要成本优化、供应商多元化、灰度迁移
选择建议:
- 小团队初期选择单模型绑定降低复杂度
- 成熟产品需要多模型以应对供应商依赖和成本波动
1.3 配置管理系统¶
模型选择策略
模型选择的实现方式如下:
from enum import Enum
from dataclasses import dataclass
from typing import Optional, List
class ModelProviderType(Enum):
CLAUDE = "claude"
OPENAI = "openai"
DEEPSEEK = "deepseek"
GEMINI = "gemini"
OLLAMA = "ollama"
@dataclass
class ModelConfig:
"""模型配置对象"""
provider: ModelProviderType
model_id: str
api_key: Optional[str] = None
api_endpoint: Optional[str] = None
timeout: int = 30
max_tokens: int = 4096
temperature: Optional[float] = None # 默认不传;部分 Claude 模型拒绝非默认采样参数
@dataclass
class ModelSelectionPolicy:
"""模型选择策略"""
primary: ModelConfig
fallback_chain: List[ModelConfig] = None
cost_threshold: float = None # 成本上限,超过则切换
latency_threshold: int = None # 延迟上限,超过则切换
1.3.1 故障转移链路¶
故障转移链路定义了模型失败时的替代方案:
flowchart TD
A["应用请求"] --> B["Primary Model"]
B --> C{成功?}
C -->|是| D["成功 ✓"]
C -->|失败/超时/配额| E["Fallback-1 Model"]
E --> F{成功?}
F -->|是| D
F -->|失败| G["Fallback-2 Model"]
G --> H{成功?}
H -->|是| D
H -->|失败| I["<b>Circuit Breaker</b><br/>返回错误 ✗"]
style A fill:#e8f4f8,stroke:#4a90a4,stroke-width:2px,color:#000000
style B fill:#e8f4e8,stroke:#4a9044,stroke-width:2px,color:#000000
style E fill:#fff4e8,stroke:#a49044,stroke-width:2px,color:#000000
style G fill:#fff4e8,stroke:#a49044,stroke-width:2px,color:#000000
style D fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px,color:#000000
style I fill:#ffcccc,stroke:#c62828,stroke-width:2px,color:#000000
实现示例:
import time
class CircuitBreaker:
"""熔断器,跟踪模型健康状态"""
def __init__(self, failure_threshold: int = 5, reset_timeout: int = 60):
self.failure_count = 0
self.failure_threshold = failure_threshold
self.reset_timeout = reset_timeout
self.state = "closed" # closed, open, half-open
self.last_failure_time = None
def record_success(self):
self.failure_count = 0
self.state = "closed"
def record_failure(self):
self.failure_count += 1
if self.failure_count >= self.failure_threshold:
self.state = "open"
self.last_failure_time = time.time()
def is_available(self) -> bool:
if self.state == "closed":
return True
if self.state == "open":
if time.time() - self.last_failure_time > self.reset_timeout:
self.state = "half-open"
return True
return False
return True # half-open 允许尝试
1.3.2 Provider 接口设计¶
Provider 接口定义了模型的核心操作。使用 Python Protocol 提供灵活的鸭子类型:
from typing import Protocol, List, Any, Optional
class Message:
def __init__(self, role: str, content: str):
self.role = role # "user", "assistant"
self.content = content
class ProviderResponse:
def __init__(self, content: str, tokens_used: int, model: str):
self.content = content
self.tokens_used = tokens_used
self.model = model
class ModelProvider(Protocol):
"""LLM 供应商的统一接口"""
def complete(
self,
messages: List[Message],
temperature: Optional[float] = None,
max_tokens: int = 4096,
) -> ProviderResponse:
"""完整的模型调用(无流式)"""
...
def stream(
self,
messages: List[Message],
temperature: Optional[float] = None,
max_tokens: int = 4096,
):
"""流式模型调用,逐块返回"""
...
def estimate_tokens(self, text: str) -> int:
"""估算文本的 token 数"""
...
def validate_config(self) -> bool:
"""验证配置有效性(API 密钥等)"""
...
1.3.3 具体实现:Claude Provider¶
Claude Provider 的具体实现如下:
import anthropic
from typing import Generator, Optional
class ClaudeProvider:
def __init__(self, config: ModelConfig):
self.config = config
self.client = anthropic.Anthropic(api_key=config.api_key)
def complete(
self,
messages: List[Message],
temperature: Optional[float] = None,
max_tokens: int = 4096,
) -> ProviderResponse:
"""Claude 完整调用"""
api_messages = [
{"role": msg.role, "content": msg.content}
for msg in messages
]
kwargs = {
"model": self.config.model_id,
"max_tokens": max_tokens,
"messages": api_messages,
}
if temperature is not None:
kwargs["temperature"] = temperature
response = self.client.messages.create(**kwargs)
return ProviderResponse(
content=response.content[0].text,
tokens_used=(
response.usage.input_tokens
+ getattr(response.usage, "cache_creation_input_tokens", 0)
+ getattr(response.usage, "cache_read_input_tokens", 0)
+ response.usage.output_tokens
),
model=self.config.model_id,
)
def stream(
self,
messages: List[Message],
temperature: Optional[float] = None,
max_tokens: int = 4096,
) -> Generator[str, None, None]:
"""Claude 流式调用"""
api_messages = [
{"role": msg.role, "content": msg.content}
for msg in messages
]
kwargs = {
"model": self.config.model_id,
"max_tokens": max_tokens,
"messages": api_messages,
}
if temperature is not None:
kwargs["temperature"] = temperature
with self.client.messages.stream(**kwargs) as stream:
for text in stream.text_stream:
yield text
def estimate_tokens(self, text: str) -> int:
"""Claude token 计数"""
response = self.client.messages.count_tokens(
model=self.config.model_id,
messages=[{"role": "user", "content": text}]
)
return response.input_tokens
def validate_config(self) -> bool:
try:
self.estimate_tokens("test")
return True
except:
return False
1.3.4 模型选择引擎¶
模型选择引擎的实现方式如下:
class ModelSelectionEngine:
def __init__(self, policy: ModelSelectionPolicy):
self.policy = policy
self.breakers = {}
self._init_breakers()
def _init_breakers(self):
for config in [self.policy.primary] + (
self.policy.fallback_chain or []
):
self.breakers[config.model_id] = CircuitBreaker()
def select_model(self) -> ModelProvider:
"""根据健康状态选择可用模型"""
candidates = [self.policy.primary] + (
self.policy.fallback_chain or []
)
for config in candidates:
breaker = self.breakers[config.model_id]
if breaker.is_available():
return self._create_provider(config)
raise Exception("所有模型不可用")
def mark_failure(self, model_id: str):
"""记录模型故障"""
if model_id in self.breakers:
self.breakers[model_id].record_failure()
def mark_success(self, model_id: str):
"""记录模型成功"""
if model_id in self.breakers:
self.breakers[model_id].record_success()
def _create_provider(self, config: ModelConfig):
if config.provider == ModelProviderType.CLAUDE:
return ClaudeProvider(config)
# ... 其他 provider 实现
1.3.5 配置文件管理¶
配置文件的结构示例如下:
{
"model_selection": {
"primary": {
"provider": "claude",
"model_id": "claude-sonnet-4-6",
"timeout": 30,
"max_tokens": 4096
},
"fallback_chain": [
{
"provider": "openai",
"model_id": "gpt-5.4",
"timeout": 30,
"max_tokens": 4096
},
{
"provider": "deepseek",
"model_id": "deepseek-chat",
"timeout": 30,
"max_tokens": 4096
}
],
"cost_threshold": 0.05,
"latency_threshold": 5000
}
}
1.3.6 模型抽象层架构图¶
模型抽象层的整体架构如下所示:
graph LR
A["应用程序"] -->|请求调用| B["模型路由器"]
B -->|选择提供商| C["Provider 适配器"]
C -->|调用 API| D["Claude"]
C -->|调用 API| E["OpenAI GPT"]
C -->|调用 API| F["Google Gemini"]
style A fill:#e1f5ff
style B fill:#fff3e0
style C fill:#f3e5f5
style D fill:#c8e6c9
style E fill:#c8e6c9
style F fill:#c8e6c9
图 7-1:模型抽象层架构 —— 应用通过统一的路由器访问多个模型提供商
1.3.7 总结¶
模型抽象层通过 Provider 接口、配置驱动的选择策略和故障转移机制,实现了:
- 灵活的多模型支持或单模型绑定
- 透明的故障切换,对上层应用无感
- 可观测的模型健康度和选择决策
- 成本与性能的可控权衡
这为后续的输出解析、质量门控奠定了基础。
1.4 能力降级与权限收缩¶
前面的故障转移链路解决的是可用性问题:主模型不可用时换一个能用的,让请求继续得到响应。但 select_model() 的判据只有 CircuitBreaker.is_available(),也就是「这个模型现在能不能调通」。它没有回答另一个问题:换上来的模型,是否仍然有能力承担原来那些自动执行的决策。
这两件事被混为一谈时,会出现一种很难察觉的生产状态:
| 维度 | 状态 |
|---|---|
| 服务可用性 | 正常,仪表板全绿 |
| 模型能力 | 已下降(上下文更小、结构化输出更弱、工具调用不稳定) |
| 智能体权限 | 未变,仍可自动执行 |
主模型故障是一次显性事件,会触发告警、进入值班流程;而回退成功之后,告警恢复、可用性指标回到 99.99%,能力下降却不会触发任何东西。系统看起来已经恢复了。
问题出在能力下降会静默改变决策的输入。假设回退模型的上下文预算更小,抽象层为了让工作流继续跑通,只能截掉一部分证据;智能体于是在一份不完整的材料上做判断。如果被截掉的恰好是一条限制性条款,而剩下的材料看起来都很正常,模型会给出一个自信、无幻觉、推理链条完整的结论——然后被自动放行。这里没有任何一个组件报错:模型没编造,回退机制按设计工作,熔断器状态正常。真正的缺陷是系统把能力降级当成了基础设施细节,而不是一次权限变更。
因此,回退策略除了定义「主模型失败后换谁」,还应该定义「能力变化时权限如何跟着变」:
from dataclasses import dataclass, field
@dataclass
class CapabilityProfile:
"""描述一个模型能稳定承担什么,而不只是能否调通"""
context_tokens: int
structured_output: str # high | moderate | limited
tool_calling: str # high | moderate | limited
evidence_reconciliation: str # high | moderate | limited
@dataclass
class AuthorityProfile:
"""该能力档位下允许的最高权限"""
may_recommend: bool = True
may_auto_approve: bool = False
may_execute_external: bool = False
# 与 ModelConfig 一一对应:能力档位决定权限上限
CAPABILITY_AUTHORITY = {
"primary": AuthorityProfile(
may_recommend=True, may_auto_approve=True, may_execute_external=True
),
"fallback": AuthorityProfile(
may_recommend=True, may_auto_approve=False, may_execute_external=False
),
}
对应地,模型选择引擎返回的就不再只是一个 provider,而是「provider + 当前可用权限」这一对值:
class AuthorityAwareSelectionEngine(ModelSelectionEngine):
def select_with_authority(self):
"""返回 (provider, authority, degraded),让上层无法忽略降级事实"""
primary = self.policy.primary
if self.breakers[primary.model_id].is_available():
return self._create_provider(primary), CAPABILITY_AUTHORITY["primary"], False
for config in self.policy.fallback_chain or []:
if self.breakers[config.model_id].is_available():
# 关键:换模型的同时换权限,二者不能分开决策
return self._create_provider(config), CAPABILITY_AUTHORITY["fallback"], True
raise RuntimeError("所有模型不可用")
这样一来,回退期间智能体仍然可以读取、分析、生成建议,只是不再拥有自动批准和对外执行的权限——服务没有中断,风险敞口却收窄了。
这里的核心判断是:可用性回退不应该静默地变成权限回退。为无状态服务做冗余时,副本和主库提供的是等价能力,切换是透明的;而不同模型之间除了性能差异,能安全承担的决策范围也不一样。所以回退链路本身属于部署策略的一部分,不只是一项基础设施配置。
由此还可以推出一个更基本的取舍:不是所有能力都应该「优雅降级」。
| 类别 | 典型任务 | 回退期间的处理 |
|---|---|---|
| 可降级 | 提取、摘要、草稿、低风险建议 | 继续执行 |
| 不可降级 | 高价值执行、不可逆操作、受监管决策 | 暂停自动路径,转人工 |
对不可降级的任务,正确的行为不是「保持服务可用」,而是明确返回「自动决策当前不可用」并转入人工审核。这与 11.3 容错模式 中「失败要显式、不要静默吞掉」是同一条原则在权限维度上的体现。
最后,降级期间的运行事实需要被记录下来,否则事后无法回答「这段时间到底发生了什么」:哪些请求是在降级状态下处理的、其中有没有本该走人工的决策被自动放行、恢复后是否需要回溯复核。可用性仪表板上的 99.99% 并不能替代这份记录,因为它衡量的是「系统有没有响应」,而不是「系统响应时是否具备原有的保障」。恢复主模型时也同样需要一道显式的门槛——健康检查通过、抽样回归通过、降级期间积压的高风险决策已经处理完毕,之后才把权限调回原位。
2. 结构化输出解析与校验¶
模型输出需要被转换为类型安全的结构化形式才能可靠地驱动后续操作。本节介绍 Claude 的消息类型系统、非流式与流式响应的解析方法、Pydantic 参数验证以及完整的解析管道设计。
2.1 核心问题¶
原始 API 响应是无类型的字符串或弱类型的 JSON。要将其安全地转换为类型安全的结构化表示,需要解决:
- 类型识别:区分文本、工具调用、思考块等不同内容类型
- 增量解析:处理流式响应的片段化更新
- 结构校验:验证解析结果的完整性与合法性
- 向后兼容:支持不同 API 版本和模型的差异
2.2 Claude 的消息类型系统¶
Claude API 返回的消息(Message)由多个内容块(ContentBlock)组成,每个块有不同的类型:
from dataclasses import dataclass
from enum import Enum
from typing import Optional, List, Any
class ContentBlockType(Enum):
TEXT = "text"
TOOL_USE = "tool_use"
THINKING = "thinking"
@dataclass
class TextBlock:
"""文本内容块"""
type: str = "text"
text: str = ""
@dataclass
class ToolUseBlock:
"""工具调用块"""
type: str = "tool_use"
id: str = ""
name: str = ""
input: dict = None # JSON 输入参数
@dataclass
class ThinkingBlock:
"""思考过程块(Adaptive Thinking)"""
type: str = "thinking"
thinking: str = ""
ContentBlock = TextBlock | ToolUseBlock | ThinkingBlock
@dataclass
class ParsedMessage:
"""解析后的完整消息"""
content_blocks: List[ContentBlock]
stop_reason: str # "end_turn", "tool_use", "max_tokens"
tokens_used: int
def text_content(self) -> str:
"""提取所有文本内容"""
texts = [
block.text for block in self.content_blocks
if isinstance(block, TextBlock)
]
return "".join(texts)
def tool_calls(self) -> List[ToolUseBlock]:
"""提取所有工具调用"""
return [
block for block in self.content_blocks
if isinstance(block, ToolUseBlock)
]
def thinking_content(self) -> Optional[str]:
"""提取思考过程"""
for block in self.content_blocks:
if isinstance(block, ThinkingBlock):
return block.thinking
return None
非流式解析¶
完整响应的解析实现方式如下:
import json
from typing import Dict, Any
class ResponseParser:
"""API 响应解析器"""
@staticmethod
def parse_response(raw_response: Dict[str, Any]) -> ParsedMessage:
"""解析完整的 API 响应"""
content_blocks = []
# 遍历响应的 content 数组
for block in raw_response.get("content", []):
block_type = block.get("type")
if block_type == "text":
content_blocks.append(TextBlock(
type="text",
text=block.get("text", "")
))
elif block_type == "tool_use":
# 工具调用:需要解析 JSON 输入
input_data = block.get("input", {})
if isinstance(input_data, str):
try:
input_data = json.loads(input_data)
except json.JSONDecodeError as e:
# 捕获 JSON 解析失败,记录错误并使用空对象
print(f"警告: JSON 解析失败: {e}, 使用空参数")
input_data = {}
content_blocks.append(ToolUseBlock(
type="tool_use",
id=block.get("id", ""),
name=block.get("name", ""),
input=input_data
))
elif block_type == "thinking":
content_blocks.append(ThinkingBlock(
type="thinking",
thinking=block.get("thinking", "")
))
return ParsedMessage(
content_blocks=content_blocks,
stop_reason=raw_response.get("stop_reason", "end_turn"),
tokens_used=(
raw_response.get("usage", {}).get("input_tokens", 0) +
raw_response.get("usage", {}).get("output_tokens", 0)
)
)
流式解析¶
流式响应分块到达,需要增量构建完整消息。关键是跟踪当前正在构建的块状态:
import json
from enum import Enum
from typing import Dict, Any, Optional
class StreamEventType(Enum):
MESSAGE_START = "message_start"
CONTENT_BLOCK_START = "content_block_start"
CONTENT_BLOCK_DELTA = "content_block_delta"
CONTENT_BLOCK_STOP = "content_block_stop"
MESSAGE_DELTA = "message_delta"
MESSAGE_STOP = "message_stop"
class StreamingParser:
"""流式响应解析器"""
def __init__(self):
self.content_blocks = []
self.current_block = None
self.block_index = 0
self.stop_reason = "end_turn"
self.tokens_used = 0
self.partial_json_buffer = "" # 累积工具参数的 JSON 分片
def process_event(self, event: Dict[str, Any]) -> Optional[ParsedMessage]:
"""处理单个流式事件"""
event_type = event.get("type")
if event_type == "message_start":
# 消息开始,重置状态
self.content_blocks = []
self.current_block = None
self.block_index = 0
usage = event.get("message", {}).get("usage", {})
self.tokens_used = (
usage.get("input_tokens", 0)
+ usage.get("cache_creation_input_tokens", 0)
+ usage.get("cache_read_input_tokens", 0)
)
elif event_type == "content_block_start":
# 新块开始
self.block_index = event.get("index", 0)
block_info = event.get("content_block", {})
block_type = block_info.get("type")
if block_type == "text":
self.current_block = TextBlock(type="text", text="")
elif block_type == "tool_use":
self.current_block = ToolUseBlock(
type="tool_use",
id=block_info.get("id", ""),
name=block_info.get("name", ""),
input={}
)
self.partial_json_buffer = "" # 参数 JSON 跨多个 delta 到达,需累积
elif block_type == "thinking":
self.current_block = ThinkingBlock(type="thinking", thinking="")
# 确保数组足够长
while len(self.content_blocks) <= self.block_index:
self.content_blocks.append(None)
elif event_type == "content_block_delta":
# 块内容增量更新
delta = event.get("delta", {})
delta_type = delta.get("type")
if delta_type == "text_delta":
if isinstance(self.current_block, TextBlock):
self.current_block.text += delta.get("text", "")
elif delta_type == "input_json_delta":
# 工具调用的 JSON 增量:单个分片(如 '{"query": "Py')通常不是
# 合法 JSON,只能累积,待 content_block_stop 时一次性解析
if isinstance(self.current_block, ToolUseBlock):
self.partial_json_buffer += delta.get("partial_json", "")
elif delta_type == "thinking_delta":
if isinstance(self.current_block, ThinkingBlock):
self.current_block.thinking += delta.get("thinking", "")
elif event_type == "content_block_stop":
# 块完成:工具块在此刻才拥有完整的参数 JSON
if isinstance(self.current_block, ToolUseBlock) and self.partial_json_buffer:
try:
self.current_block.input = json.loads(self.partial_json_buffer)
except json.JSONDecodeError:
self.current_block.input = {}
self.partial_json_buffer = ""
# 保存到数组
if self.current_block is not None:
self.content_blocks[self.block_index] = self.current_block
self.current_block = None
elif event_type == "message_delta":
# 消息完成
delta = event.get("delta", {})
self.stop_reason = delta.get("stop_reason", "end_turn")
usage = event.get("usage", {})
self.tokens_used += usage.get("output_tokens", 0)
# 返回完整的解析结果
return ParsedMessage(
content_blocks=[b for b in self.content_blocks if b],
stop_reason=self.stop_reason,
tokens_used=self.tokens_used
)
return None # 消息未完成
使用流式解析器¶
流式解析器的具体使用方式如下:
import json
class StreamIterator:
"""流式迭代器,隐藏解析复杂性"""
def __init__(self, api_stream):
self.api_stream = api_stream
self.parser = StreamingParser()
def __iter__(self):
for event_data in self.api_stream:
try:
event = json.loads(event_data)
except json.JSONDecodeError as e:
# JSON 解析失败,记录错误并跳过该事件
print(f"警告: 流式事件 JSON 解析失败: {e}, 跳过该事件")
continue
result = self.parser.process_event(event)
if result is not None:
yield result
# 使用示例:api_stream 应是“逐条 JSON 字符串”的原始事件流(如 SSE 的 data: 行)。
# 注意:anthropic SDK 的 client.messages.stream() 产出的是已解析的事件对象,
# 直接遍历即可,不应再传入本迭代器做 json.loads。
for parsed_message in StreamIterator(raw_sse_data_lines):
print(f"接收到消息: {parsed_message.text_content()}")
for tool_call in parsed_message.tool_calls():
print(f" 工具调用: {tool_call.name}({tool_call.input})")
Pydantic 校验¶
使用 Pydantic 定义预期的工具参数结构,并在解析后进行验证:
from pydantic import BaseModel, ValidationError, field_validator
from typing import Optional
class SearchToolInput(BaseModel):
"""搜索工具的输入参数"""
query: str
max_results: int = 10
language: str = "en"
@field_validator("max_results")
@classmethod
def validate_max_results(cls, v):
if v < 1 or v > 100:
raise ValueError("max_results 必须在 1-100 之间")
return v
class ToolCallValidator:
"""工具调用参数验证"""
TOOL_SCHEMAS = {
"search": SearchToolInput,
"database_query": "DatabaseQueryInput",
# ... 其他工具
}
@staticmethod
def validate_tool_call(tool_call: ToolUseBlock) -> bool:
"""验证工具调用的合法性"""
tool_name = tool_call.name
if tool_name not in ToolCallValidator.TOOL_SCHEMAS:
raise ValueError(f"未知的工具: {tool_name}")
schema = ToolCallValidator.TOOL_SCHEMAS[tool_name]
try:
schema(**tool_call.input)
return True
except ValidationError as e:
# 捕获 Pydantic 验证错误,提供详细错误信息
error_msg = "; ".join([f"{err['loc']}: {err['msg']}" for err in e.errors()])
raise ValueError(f"工具参数验证失败: {error_msg}")
except (TypeError, AttributeError) as e:
# 捕获参数类型错误
raise ValueError(f"工具调用格式错误: {e}")
# 使用
tool_call = ToolUseBlock(
id="call_123",
name="search",
input={"query": "Python async", "max_results": 10}
)
try:
if ToolCallValidator.validate_tool_call(tool_call):
print("工具调用有效")
except ValueError as e:
print(f"验证失败: {e}")
解析管道¶
完整的解析管道实现如下:
class ParsingPipeline:
"""完整的解析管道"""
def __init__(self):
self.parser = ResponseParser()
self.validator = ToolCallValidator()
def parse_and_validate(
self, raw_response: Dict[str, Any]
) -> ParsedMessage:
"""解析并验证响应"""
# 步骤 1:解析
parsed = self.parser.parse_response(raw_response)
# 步骤 2:验证工具调用
for tool_call in parsed.tool_calls():
self.validator.validate_tool_call(tool_call)
# 步骤 3:检查完整性
if parsed.stop_reason == "max_tokens":
raise ValueError("响应因 token 限制被截断")
return parsed
输出解析管道架构¶
结构化输出的解析流程如下所示:
graph LR
A["原始响应"] -->|解析内容块| B["流式解析器"]
B -->|验证类型| C["类型验证器"]
C -->|构建消息| D["结构化输出"]
E["完整响应"] -.->|也支持| B
style A fill:#ffebee
style B fill:#fff9c4
style C fill:#e0f2f1
style D fill:#c8e6c9
图 7-2:输出解析管道 —— 从原始响应逐步转换为类型安全的结构化数据
总结¶
结构化输出解析通过:
- 类型系统 (TextBlock/ToolUseBlock/ThinkingBlock)提供类型安全
- 非流式与流式双路径 支持不同使用场景
- 增量解析 实现高效的流式处理
- Pydantic 校验 确保参数合法性
这是构建可靠智能体的基础,为下一步的质量门控做准备。
3. 输出质量门控与过滤¶
工具执行前需要多层验证防线来确保安全和有效性。本节介绍六层质量门控机制、规划-生成-评估三角色分离模式,以及如何通过独立评估者消除自我确认偏差。
3.1 问题定义¶
工具调用前的最后防线是质量门控(Quality Gate)。未经验证的输出可能导致:
- 参数错误:工具接收非法数据导致执行失败
- 资源泄露:过度调用导致费用飙升或系统宕机
- 格式异常:API 调用失败导致 智能体循环重试
- 安全漏洞:注入攻击、不合理的权限操作
质量门控在 执行之前 进行多层验证,确保只有安全、合法的调用才能通过。
其中“权限检查”调用第 12 章的统一策略决策服务;质量门控不维护第二套权限政策,只消费同一个决策结果。
3.2 门控层次结构¶
质量门控按以下六个层次依次进行验证:
graph TD
A["原始响应"] -->|检查格式| B["格式检查"]
B -->|验证存在| C["工具存在性"]
C -->|检查类型| D["参数验证"]
D -->|检查逻辑| E["业务逻辑"]
E -->|检查权限| F["权限检查"]
F -->|检查注入| I["注入检测"]
I -->|通过所有检查| G["执行工具"]
B -.->|失败| H["拒绝执行"]
C -.->|失败| H
D -.->|失败| H
E -.->|失败| H
F -.->|失败| H
I -.->|失败| H
style A fill:#ffebee
style B fill:#fff9c4
style C fill:#fff9c4
style D fill:#fff9c4
style E fill:#fff9c4
style F fill:#fff9c4
style I fill:#fff9c4
style G fill:#c8e6c9
style H fill:#ffcdd2
图 7-3:质量门控多层验证流程 —— 工具执行前的六层递进式防护
1) 基础格式检查¶
验证解析结果的完整性和一致性:
from dataclasses import dataclass
from enum import Enum
from typing import Optional, List
class ValidationResult(Enum):
PASS = "pass"
FAIL = "fail"
WARN = "warn"
@dataclass
class ValidationReport:
result: ValidationResult
errors: List[str]
warnings: List[str]
suggestion: Optional[str] = None
class FormatValidator:
"""基础格式验证"""
@staticmethod
def validate_tool_use_block(tool_use) -> ValidationReport:
"""验证工具调用块的结构完整性"""
errors = []
warnings = []
# 检查必填字段
if not tool_use.id:
errors.append("tool_use.id 不能为空")
if not tool_use.name:
errors.append("tool_use.name 不能为空")
if tool_use.input is None:
errors.append("tool_use.input 不能为空")
# 检查 ID 格式(Claude 的 tool_use.id 以 "toolu_" 开头)
if tool_use.id and not tool_use.id.startswith("toolu_"):
warnings.append(
f"非标准 tool_use.id: {tool_use.id}"
)
# 检查名称格式(应为 snake_case)
if tool_use.name and not FormatValidator._is_valid_name(tool_use.name):
warnings.append(
f"工具名格式非标准: {tool_use.name}"
)
if errors:
return ValidationReport(
result=ValidationResult.FAIL,
errors=errors,
warnings=warnings,
suggestion="检查 API 响应是否正确解析"
)
if warnings:
return ValidationReport(
result=ValidationResult.WARN,
errors=[],
warnings=warnings
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)
@staticmethod
def _is_valid_name(name: str) -> bool:
"""检查工具名是否符合 snake_case"""
return name.islower() and name.replace("_", "").isalnum()
2) 工具存在性检查¶
验证工具是否已注册:
class ToolRegistry:
"""工具注册表与存在性检查"""
def __init__(self):
self.tools = {}
def register(self, name: str, handler, schema):
"""注册工具"""
self.tools[name] = {
"handler": handler,
"schema": schema,
"enabled": True
}
def is_tool_available(self, name: str) -> bool:
"""检查工具是否可用"""
if name not in self.tools:
return False
return self.tools[name]["enabled"]
def get_tool_schema(self, name: str):
"""获取工具的参数 schema"""
if name not in self.tools:
return None
return self.tools[name]["schema"]
class ExistenceValidator:
"""工具存在性验证"""
def __init__(self, registry: ToolRegistry):
self.registry = registry
def validate(self, tool_name: str) -> ValidationReport:
"""检查工具是否存在且可用"""
errors = []
suggestions = []
if tool_name not in self.registry.tools:
errors.append(f"工具 '{tool_name}' 未注册")
# 生成相似工具建议
similar = self._find_similar_tools(tool_name)
if similar:
suggestions.append(
f"您是想用 '{similar[0]}' 吗?"
)
elif not self.registry.is_tool_available(tool_name):
errors.append(f"工具 '{tool_name}' 已禁用")
if errors:
return ValidationReport(
result=ValidationResult.FAIL,
errors=errors,
warnings=[],
suggestion=suggestions[0] if suggestions else None
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)
def _find_similar_tools(self, name: str) -> List[str]:
"""使用编辑距离找相似工具名"""
from difflib import get_close_matches
available = [n for n in self.registry.tools.keys()
if self.registry.is_tool_available(n)]
return get_close_matches(name, available, n=1, cutoff=0.6)
3) 参数类型和范围检查¶
使用 Pydantic 进行参数类型和范围验证的实现如下:
from pydantic import BaseModel, ValidationError, field_validator
from typing import Optional, Any
class FileReadInput(BaseModel):
"""文件读取工具的输入"""
file_path: str
encoding: str = "utf-8"
max_lines: Optional[int] = None
@field_validator("file_path")
@classmethod
def validate_file_path(cls, v):
if not isinstance(v, str) or len(v) == 0:
raise ValueError("file_path 必须是非空字符串")
return v
@field_validator("max_lines")
@classmethod
def validate_max_lines(cls, v):
if v is not None and (v < 1 or v > 100000):
raise ValueError("max_lines 必须在 1-100000 之间")
return v
class ParameterValidator:
"""参数验证器"""
def __init__(self, registry: ToolRegistry):
self.registry = registry
def validate(self, tool_name: str, parameters: dict) -> ValidationReport:
"""验证工具参数"""
schema = self.registry.get_tool_schema(tool_name)
if schema is None:
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"无法找到工具 {tool_name} 的 schema"],
warnings=[]
)
errors = []
warnings = []
try:
# 使用 Pydantic 验证参数
validated = schema(**parameters)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)
except ValidationError as e:
for error in e.errors():
field = ".".join(str(x) for x in error["loc"])
msg = error["msg"]
errors.append(f"参数 {field}: {msg}")
if errors:
return ValidationReport(
result=ValidationResult.FAIL,
errors=errors,
warnings=warnings,
suggestion="请检查工具的参数定义"
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=warnings
)
4) 业务逻辑检查¶
某些检查超越类型系统,涉及业务规则:
class BusinessLogicValidator:
"""业务逻辑验证"""
def __init__(self):
self.config = {
"max_api_calls_per_minute": 100,
"max_file_size_mb": 10,
"allowed_domains": ["example.com", "api.service.com"],
}
def validate_api_call_frequency(self, tool_name: str,
call_history: List[float]) -> ValidationReport:
"""检查 API 调用频率是否超过限制"""
import time
now = time.time()
# 计算最近 1 分钟的调用数
recent_calls = [t for t in call_history if now - t < 60]
limit = self.config["max_api_calls_per_minute"]
if len(recent_calls) >= limit:
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"工具 {tool_name} 在 1 分钟内调用已达 {limit} 次限制"],
warnings=[],
suggestion="请稍后再试"
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)
def validate_file_access(self, file_path: str) -> ValidationReport:
"""检查文件访问权限和大小"""
import os
errors = []
# 检查文件存在性
if not os.path.exists(file_path):
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"文件不存在: {file_path}"],
warnings=[]
)
# 检查文件大小
file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
max_size = self.config["max_file_size_mb"]
if file_size_mb > max_size:
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"文件大小 {file_size_mb:.2f}MB 超过限制 {max_size}MB"],
warnings=[]
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)
def validate_http_request(self, url: str) -> ValidationReport:
"""检查 HTTP 请求的目标合法性"""
from urllib.parse import urlparse
parsed = urlparse(url)
domain = parsed.netloc
if domain not in self.config["allowed_domains"]:
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"不允许访问域名: {domain}"],
warnings=[],
suggestion=f"允许的域名: {self.config['allowed_domains']}"
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)
5) 安全与权限检查¶
安全与权限检查的实现方式如下:
from enum import Enum
class Permission(Enum):
READ = "read"
WRITE = "write"
DELETE = "delete"
EXECUTE = "execute"
class SecurityValidator:
"""安全与权限检查"""
def __init__(self, user_permissions: set):
self.user_permissions = user_permissions
def validate_tool_permission(self, tool_name: str) -> ValidationReport:
"""检查用户是否有权调用该工具"""
# 定义工具所需权限
tool_permissions = {
"read_file": {Permission.READ},
"write_file": {Permission.WRITE},
"delete_file": {Permission.DELETE},
"execute_command": {Permission.EXECUTE},
}
if tool_name not in tool_permissions:
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"未知工具: {tool_name}"],
warnings=[]
)
required = tool_permissions[tool_name]
if not required.issubset(self.user_permissions):
missing = required - self.user_permissions
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"缺少权限: {', '.join(p.value for p in missing)}"],
warnings=[]
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)
def validate_parameter_injection(self, parameters: dict) -> ValidationReport:
"""检查参数是否包含注入攻击"""
dangerous_patterns = [
"rm -rf",
"DROP TABLE",
"<script>",
"${jndi:",
]
for key, value in parameters.items():
if isinstance(value, str):
for pattern in dangerous_patterns:
if pattern in value:
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"参数 {key} 包含危险模式: {pattern}"],
warnings=[],
suggestion="检查参数是否被恶意注入"
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)
6) 完整的质量门控引擎¶
整合六层验证逻辑的完整质量门控引擎如下:
class QualityGate:
"""完整的质量门控引擎"""
def __init__(self, registry: ToolRegistry, user_permissions: set):
self.format_validator = FormatValidator()
self.existence_validator = ExistenceValidator(registry)
self.parameter_validator = ParameterValidator(registry)
self.business_validator = BusinessLogicValidator()
self.security_validator = SecurityValidator(user_permissions)
def validate_tool_call(self, tool_call, call_history: List[float] = None):
"""执行完整的质量门控"""
results = []
# 门控 1: 格式检查
r1 = self.format_validator.validate_tool_use_block(tool_call)
results.append(("格式检查", r1))
if r1.result == ValidationResult.FAIL:
return self._report_failure(results)
# 门控 2: 工具存在性
r2 = self.existence_validator.validate(tool_call.name)
results.append(("工具存在性", r2))
if r2.result == ValidationResult.FAIL:
return self._report_failure(results)
# 门控 3: 参数验证
r3 = self.parameter_validator.validate(
tool_call.name, tool_call.input
)
results.append(("参数验证", r3))
if r3.result == ValidationResult.FAIL:
return self._report_failure(results)
# 门控 4: 业务逻辑(对文件类参数执行业务规则)
file_path = (tool_call.input or {}).get("file_path")
if file_path:
r4 = self.business_validator.validate_file_access(file_path)
results.append(("业务逻辑", r4))
if r4.result == ValidationResult.FAIL:
return self._report_failure(results)
# 门控 5: 权限检查
r5 = self.security_validator.validate_tool_permission(tool_call.name)
results.append(("权限检查", r5))
if r5.result == ValidationResult.FAIL:
return self._report_failure(results)
# 门控 6: 参数注入检测
r6 = self.security_validator.validate_parameter_injection(
tool_call.input or {}
)
results.append(("注入检测", r6))
if r6.result == ValidationResult.FAIL:
return self._report_failure(results)
return {
"passed": True,
"details": results
}
def _report_failure(self, results):
return {
"passed": False,
"details": results
}
7) 质量门控小结¶
质量门控通过多层验证机制:
- 格式检查:确保结构完整
- 存在性检查:验证工具已注册
- 参数检查:类型和范围验证
- 业务逻辑:应用特定规则
- 权限检查:阻止越权调用
- 注入检测:识别参数中的注入攻击
有效降低执行失败、资源泄露和安全风险,是智能体可靠性的关键保障。
3.3 规划-生成-评估三角色分离¶
在传统的单一模型智能体中,智能体需要同时完成三项职责:分析任务、生成解决方案、评估结果。这导致一个常见问题:自我确认偏差——生成器很难公正地评估自己的工作。
Anthropic 提出的 规划-生成-评估(Planner-Generator-Evaluator, PGE)模式 通过角色分离和潜在的不同模型配置,显著提升输出质量。
3.3.1 三角色的职责划分¶
规划者(Planner)
- 分析任务,理解用户意图
- 将复杂目标分解为子任务
- 创建执行计划和验证标准
- 使用较高的推理预算(可选使用 Adaptive Thinking)
生成者(Generator)
- 按照计划逐步执行
- 调用工具、处理 API 响应
- 产生原始输出
- 优化吞吐量和成本(使用轻量级模型配置)
评估者(Evaluator)
- 独立检查生成器的输出
- 验证是否满足规划阶段的要求
- 判断是否需要修正或重新生成
- 必须使用独立的提示和上下文 (避免自我确认偏差)
3.3.2 架构流程¶
质量门架构采用三角验证模式,通过独立的评估者对生成的结果进行验证:
graph LR
A["用户请求"] -->|分析意图| B["<b>规划者</b><br/>生成执行计划"]
B -->|步骤序列<br/>验证标准| C["<b>生成者</b><br/>执行任务"]
C -->|原始输出| D["<b>评估者</b><br/>独立验证"]
D -->|是否通过| E{结果判断}
E -->|通过| F["返回结果"]
E -->|失败| G["反馈给生成者"]
G -->|修正方案| C
E -->|多次失败| H["反馈给规划者"]
H -->|重新规划| B
style A fill:#e3f2fd
style B fill:#fff3e0
style C fill:#e8f5e9
style D fill:#f3e5f5
style E fill:#fce4ec
style F fill:#c8e6c9
style G fill:#ffcdd2
style H fill:#ffcdd2
3.3.3 关键设计原则¶
1. 评估者的独立性
评估者必须与生成者完全隔离,这是 PGE 模式的核心:
class PGEAgent:
"""规划-生成-评估三角色分离"""
def __init__(self, planner_model, generator_model, evaluator_model):
self.planner = planner_model # 高推理预算
self.generator = generator_model # 优化吞吐量
self.evaluator = evaluator_model # 独立验证
async def execute(self, user_request: str) -> ExecutionResult:
"""执行PGE流程"""
# 第一步:规划
print("[Phase 1] Planning...")
plan = await self.planner.analyze_and_plan(user_request)
print(f"Plan: {plan.steps}")
# 第二步:生成
print("[Phase 2] Generating...")
output = await self.generator.execute_plan(plan)
# 第三步:评估 - 关键:使用独立的评估者
print("[Phase 3] Evaluating...")
evaluation = await self.evaluate_output(
output=output,
plan=plan,
evaluator=self.evaluator # 独立的模型
)
if evaluation.passed:
return output
# 失败时的重试逻辑
if evaluation.retry_generator:
# 让生成者修正(基于评估反馈)
output_v2 = await self.generator.refine(
original_output=output,
feedback=evaluation.feedback,
plan=plan
)
evaluation_v2 = await self.evaluate_output(
output=output_v2,
plan=plan,
evaluator=self.evaluator
)
if evaluation_v2.passed:
return output_v2
if evaluation.retry_planner:
# 让规划者重新规划(当多次生成都失败时)
plan_v2 = await self.planner.replan(
user_request=user_request,
failure_reason=evaluation.failure_analysis
)
# 重新执行生成
return await self.execute_with_plan(plan_v2)
raise ExecutionError(f"Failed after retries: {evaluation.feedback}")
async def execute_with_plan(self, plan) -> ExecutionResult:
"""跳过规划阶段,按给定计划执行生成与评估(重新规划后的入口)"""
output = await self.generator.execute_plan(plan)
evaluation = await self.evaluate_output(
output=output, plan=plan, evaluator=self.evaluator
)
if evaluation.passed:
return output
raise ExecutionError(f"Replanned execution failed: {evaluation.feedback}")
async def evaluate_output(
self,
output: str,
plan: Plan,
evaluator: Model
) -> EvaluationResult:
"""独立评估输出"""
# 构造评估提示 - 这是关键:评估者不应该看到生成过程
evaluation_prompt = f"""
Please evaluate the following output against the original requirements.
Original Requirements:
{plan.requirements}
Verification Criteria:
{plan.verification_criteria}
Output to Evaluate:
{output}
Answer:
1. Does the output meet ALL requirements? (yes/no)
2. Are there any errors or issues?
3. If failed, what specific improvements are needed?
4. Confidence level: (high/medium/low)
"""
evaluation_text = await evaluator.infer(evaluation_prompt)
return self._parse_evaluation(evaluation_text, plan)
def _parse_evaluation(self, eval_text: str, plan: Plan) -> EvaluationResult:
"""解析评估结果"""
# 简化实现:检查评估是否包含肯定
passed = "yes" in eval_text.lower()
# 判断是重试生成还是重新规划
retry_generator = "small" in eval_text.lower() or "adjust" in eval_text.lower()
retry_planner = "fundamental" in eval_text.lower() or "rethink" in eval_text.lower()
return EvaluationResult(
passed=passed,
retry_generator=retry_generator,
retry_planner=retry_planner,
feedback=eval_text
)
2. 模型配置的灵活性
虽然理想情况下三个角色使用不同的模型,但也可以使用同一模型的不同配置:
# 配置示例
# 注意:Opus 4.7 起已移除 temperature/top_p/top_k,传入会返回 400;
# 推理深度改用 adaptive thinking + effort 控制。Sonnet 4.6 仍接受采样参数。
PLANNER_CONFIG = {
"model": "claude-opus-4-7", # 最强模型
"adaptive_thinking": True, # 高推理预算
"effort": "high", # 取代 temperature 的确定性/深度旋钮
}
GENERATOR_CONFIG = {
"model": "claude-sonnet-4-6", # 平衡成本/性能
"adaptive_thinking": False, # 轻量级,专注执行
"temperature": 0.1, # 4.6 仍支持采样参数
}
EVALUATOR_CONFIG = {
"model": "claude-opus-4-7", # 同样强大的模型
# 但使用完全不同的上下文和提示
"adaptive_thinking": True,
"effort": "high", # 同上:4.7 不接受 temperature
}
3. 避免自我确认偏差
确保评估者不会无意中受到生成者的影响:
警告:LLM-as-Judge 的隐性偏差 独立评估器虽然能减少自我确认偏差,但仍需防范 LLM 本身的评估偏见:
- 位置偏差(Position Bias):倾向于偏好开头或结尾的内容
- 长度偏好(Length Preference):可能无意识地倾向较长或较短的答案
- 风格偏好(Style Bias):倾向支持与其训练数据相似的表达风格
建议采用多评估者投票制或定期人工抽样验证来规避这些风险。
class ObjectiveEvaluator:
"""客观评估器"""
def __init__(self, evaluator_model):
self.evaluator = evaluator_model
async def evaluate(self, output: str, criteria: str) -> bool:
"""评估输出是否符合标准"""
# 关键:评估提示必须是通用的,不提及"生成器"
# 而是直接针对需求和输出
prompt = f"""
Review this output against the criteria:
Criteria:
{criteria}
Output:
{output}
Does it meet the criteria? List any gaps or issues.
"""
# 注意:不包含任何关于"生成过程"的信息
evaluation = await self.evaluator.infer(prompt)
return self._judge_pass_fail(evaluation)
def _judge_pass_fail(self, eval_text: str) -> bool:
"""判断是否通过 - 使用保守的标准"""
# 如果有任何怀疑,就认为失败
has_issues = len([l for l in eval_text.split('\n') if l.strip()]) > 1
return not has_issues
# 警告: LLM-as-Judge 偏差风险
# 评估器可能存在以下隐性偏差:
# 1. 位置偏差: 更倾向评估靠前或靠后的内容
# 2. 长度偏好: 倾向偏好较长或较短的输出
# 3. Self-Preference: 倾向支持与其训练数据相似的风格
# 建议使用多个独立评估者或人工抽样验证来规避这些风险
3.3.4 实际应用案例¶
在一个代码审查智能体中应用 PGE 模式:
1. 规划者:分析代码,生成审查清单
- 安全检查点
- 性能考量
- 可维护性标准
2. 生成者:执行审查
- 逐行检查
- 查找问题
- 生成初稿反馈
3. 评估者:独立验证
- 反馈是否涵盖了所有清单项
- 是否有遗漏的严重问题
- 反馈是否具体可行
3.3.5 PGE vs 单一模型的对比¶
| 特性 | 单一模型 | PGE 模式 |
|---|---|---|
| 自我确认偏差 | 高 | 低(独立评估) |
| 错误率 | 基线 | 显著下降 |
| 成本 | 基线 | 有所增加(多次模型调用) |
| 调试难度 | 中等 | 低(清晰的问题定位) |
3.3.6 规划-生成-评估小结¶
PGE 模式的价值在于:
- 隔离关注点:三个角色各司其职
- 独立验证:评估者的独立性确保发现问题
- 灵活扩展:可根据需求调整模型选择
- 可调试性:清晰的故障路径便于问题定位
4. 幻觉检测与工具调用验证¶
幻觉在智能体中的危害远超聊天场景,因为直接转化为可执行的操作。本节阐述幻觉的三类防线、工具名检测、参数范围验证、事实核查机制,以及配合自修正的完整幻觉检测引擎。
4.1 智能体场景中的幻觉危害¶
幻觉(Hallucination)在智能体中危害极大,因为输出直接转化为行动:
| 幻觉类型 | 示例 | 后果 |
|---|---|---|
| 工具幻觉 | 调用不存在的 send_email_to_ceo() |
调用失败,错误消息反复重试 |
| 参数幻觉 | file_path="/root/.ssh/id_rsa" |
访问敏感文件或注入攻击 |
| 事实幻觉 | “API 端点是 example.com/api/v2”(实际是 v1) | 请求错误,数据丢失 |
| 能力幻觉 | 声称可以执行“删除数据库”操作(无权限) | 执行失败,暴露权限漏洞 |
关键特点:
- 不可撤销性:工具执行后难以回滚(删除、转账等)
- 级联失败:单个幻觉引发智能体反复重试,消耗 token
- 安全漏洞:幻觉可能绕过权限检查
4.2 幻觉检测的三层防线¶
幻觉检测包括三层递进式的防线机制:
graph TD
A["模型输出"] -->|检查工具名| B["工具名幻觉检测"]
B -->|检查参数| C["参数幻觉检测"]
C -->|检查事实| D["事实幻觉检测"]
D -->|验证通过| E["执行工具调用"]
B -.->|检测到幻觉| F["纠正信息反馈"]
C -.->|检测到幻觉| F
D -.->|检测到幻觉| F
style A fill:#ffebee
style B fill:#fff3e0
style C fill:#fff3e0
style D fill:#fff3e0
style E fill:#c8e6c9
style F fill:#ffcdd2
图 7-4:幻觉检测三层防线 —— 从工具调用验证到事实核实的多层防护
层 1: 工具名幻觉检测¶
直接检查工具是否在注册表中存在:
from typing import List, Optional
from dataclasses import dataclass
@dataclass
class HallucinationDetectionResult:
is_hallucination: bool
confidence: float # 0.0-1.0
hallucination_type: str # "tool_name", "parameter", "fact"
evidence: str
correction: Optional[str] = None
class ToolNameHallucinationDetector:
"""工具名幻觉检测"""
def __init__(self, registry):
self.registry = registry
def detect(self, tool_name: str) -> HallucinationDetectionResult:
"""检测工具名是否存在"""
# 精确匹配
if self.registry.is_tool_available(tool_name):
return HallucinationDetectionResult(
is_hallucination=False,
confidence=1.0,
hallucination_type="none",
evidence=""
)
# 不存在,可能是:
# 1. 完全幻觉
# 2. 拼写错误(需纠正)
# 使用编辑距离查找最相似的工具
from difflib import SequenceMatcher
available_tools = self.registry.get_available_tools()
matches = [
(tool, SequenceMatcher(None, tool_name, tool).ratio())
for tool in available_tools
]
matches.sort(key=lambda x: x[1], reverse=True)
if matches and matches[0][1] > 0.6:
# 可能是拼写错误
corrected = matches[0][0]
return HallucinationDetectionResult(
is_hallucination=True,
confidence=0.9,
hallucination_type="tool_name",
evidence=f"工具 '{tool_name}' 不存在",
correction=f"您可能想调用 '{corrected}'"
)
else:
# 完全幻觉
return HallucinationDetectionResult(
is_hallucination=True,
confidence=0.95,
hallucination_type="tool_name",
evidence=f"工具 '{tool_name}' 完全不存在",
correction=f"可用工具: {', '.join(available_tools[:5])}"
)
层 2: 参数幻觉检测¶
参数可能虽然合法但不合理(例如超过实际限制):
class ParameterHallucinationDetector:
"""参数幻觉检测"""
def __init__(self, registry):
self.registry = registry
def detect(self, tool_name: str, parameters: dict) -> List[HallucinationDetectionResult]:
"""检测参数是否存在幻觉"""
results = []
schema = self.registry.get_tool_schema(tool_name)
if schema is None:
return results
# 检查每个参数
for param_name, param_value in parameters.items():
# 未定义的参数
if param_name not in schema.get("properties", {}):
results.append(HallucinationDetectionResult(
is_hallucination=True,
confidence=0.8,
hallucination_type="parameter",
evidence=f"参数 '{param_name}' 未在工具定义中",
correction=f"允许的参数: {list(schema['properties'].keys())}"
))
continue
# 参数值范围检查
prop_def = schema["properties"][param_name]
# 数值范围
if "minimum" in prop_def or "maximum" in prop_def:
if isinstance(param_value, (int, float)):
min_val = prop_def.get("minimum")
max_val = prop_def.get("maximum")
if min_val is not None and param_value < min_val:
results.append(HallucinationDetectionResult(
is_hallucination=True,
confidence=0.9,
hallucination_type="parameter",
evidence=f"参数 '{param_name}' 值 {param_value} 小于最小值 {min_val}",
correction=f"请使用不小于 {min_val} 的值"
))
if max_val is not None and param_value > max_val:
results.append(HallucinationDetectionResult(
is_hallucination=True,
confidence=0.9,
hallucination_type="parameter",
evidence=f"参数 '{param_name}' 值 {param_value} 大于最大值 {max_val}",
correction=f"请使用不大于 {max_val} 的值"
))
# 枚举值检查
if "enum" in prop_def:
if param_value not in prop_def["enum"]:
results.append(HallucinationDetectionResult(
is_hallucination=True,
confidence=0.95,
hallucination_type="parameter",
evidence=f"参数 '{param_name}' 值 '{param_value}' 不在允许列表中",
correction=f"允许的值: {prop_def['enum']}"
))
return results
层 3: 事实幻觉检测¶
检查输出中的事实陈述是否与已知知识库一致:
from abc import ABC, abstractmethod
class FactChecker(ABC):
"""事实检查器基类"""
@abstractmethod
def check(self, statement: str) -> bool:
"""检查陈述是否为真"""
pass
class APIEndpointChecker(FactChecker):
"""检查 API 端点的真实性"""
def __init__(self, known_endpoints: dict):
self.known_endpoints = known_endpoints
def check(self, endpoint: str) -> bool:
"""检查端点是否确实存在"""
return endpoint in self.known_endpoints.values()
class PermissionChecker(FactChecker):
"""检查权限声明的真实性"""
def __init__(self, user_id: str, permission_store):
self.user_id = user_id
self.permission_store = permission_store
def check(self, statement: str) -> bool:
"""检查权限声明是否准确"""
# 例如:检查 "我有权删除数据库" 是否为真
from re import findall, IGNORECASE
# 正则需与 detect() 构造的中文 statement(如"删除数据库 ...")保持语言一致
perms = findall(r"删除.*数据库", statement, flags=IGNORECASE)
if not perms:
return True # 无权限声明,跳过
actual_perms = self.permission_store.get_user_permissions(self.user_id)
return "delete:database" in actual_perms
class FactHallucinationDetector:
"""事实幻觉检测"""
def __init__(self, fact_checkers: dict):
self.checkers = fact_checkers
def detect(self, tool_name: str, parameters: dict) -> List[HallucinationDetectionResult]:
"""检测参数中的事实幻觉"""
results = []
# 针对特定工具的事实检查
if tool_name == "api_call" and "endpoint" in parameters:
checker = self.checkers.get("endpoint")
if checker:
endpoint = parameters["endpoint"]
if not checker.check(endpoint):
results.append(HallucinationDetectionResult(
is_hallucination=True,
confidence=0.85,
hallucination_type="fact",
evidence=f"API 端点 '{endpoint}' 在知识库中不存在",
correction="请使用已知的 API 端点"
))
if tool_name == "delete_database":
checker = self.checkers.get("permission")
if checker:
stmt = f"删除数据库 {parameters.get('database', '')}"
if not checker.check(stmt):
results.append(HallucinationDetectionResult(
is_hallucination=True,
confidence=0.9,
hallucination_type="fact",
evidence="用户权限不足",
correction="无法执行删除操作"
))
return results
完整的幻觉检测引擎¶
整合三层防线的完整幻觉检测引擎实现如下:
class HallucinationDetectionEngine:
"""完整的幻觉检测引擎"""
def __init__(self, registry, fact_checkers: dict = None):
self.tool_detector = ToolNameHallucinationDetector(registry)
self.param_detector = ParameterHallucinationDetector(registry)
self.fact_detector = FactHallucinationDetector(
fact_checkers or {}
)
def detect_all(self, tool_call) -> List[HallucinationDetectionResult]:
"""执行完整的幻觉检测"""
results = []
# 层 1: 工具名
r1 = self.tool_detector.detect(tool_call.name)
if r1.is_hallucination:
results.append(r1)
return results # 工具不存在,无需继续检查
# 层 2: 参数
r2s = self.param_detector.detect(tool_call.name, tool_call.input)
results.extend(r2s)
# 层 3: 事实
r3s = self.fact_detector.detect(tool_call.name, tool_call.input)
results.extend(r3s)
return results
自修正机制¶
当检测到幻觉时,系统不是直接失败,而是将纠正信息反馈给模型,让其自我修正:
class SelfCorrectionHandler:
"""幻觉自修正处理器"""
def __init__(self, model_provider):
self.model = model_provider
def attempt_correction(
self,
original_tool_call,
hallucination_results: List[HallucinationDetectionResult],
conversation_history: list
) -> Optional[dict]:
"""尝试让模型自我修正幻觉"""
# 构建纠正提示
correction_message = self._build_correction_prompt(
original_tool_call,
hallucination_results
)
# 添加到对话历史
conversation_history.append({
"role": "user",
"content": correction_message
})
# 请求模型重新生成
try:
response = self.model.complete(conversation_history)
return {
"corrected": True,
"original": original_tool_call,
"suggestion": response.content,
"attempts": 1
}
except Exception as e:
return {
"corrected": False,
"error": str(e)
}
def _build_correction_prompt(
self,
tool_call,
results: List[HallucinationDetectionResult]
) -> str:
"""构建纠正提示信息"""
prompt = "我发现您上一条消息中存在以下问题,请重新尝试:\n\n"
for result in results:
prompt += f"- {result.evidence}\n"
if result.correction:
prompt += f" 建议: {result.correction}\n"
prompt += "\n请重新调用正确的工具。"
return prompt
流程示例¶
完整的幻觉检测使用流程示例如下:
# 使用完整幻觉检测
engine = HallucinationDetectionEngine(registry, fact_checkers)
tool_call = ToolUseBlock(
id="call_123",
name="send_email_to_ceo", # 幻觉的工具
input={"message": "Hello"}
)
hallucinations = engine.detect_all(tool_call)
if hallucinations:
handler = SelfCorrectionHandler(model_provider)
correction = handler.attempt_correction(
tool_call,
hallucinations,
conversation_history
)
if correction["corrected"]:
print(f"模型建议: {correction['suggestion']}")
# 等待用户确认或重新执行
else:
print(f"无法自动纠正: {correction['error']}")
# 返回错误给用户
else:
print("工具调用通过验证,可以执行")
execute_tool(tool_call)
总结¶
幻觉检测的三层防线:
- 工具名检查:高置信度检测,支持纠正建议
- 参数检查:类型和范围验证
- 事实检查:知识库对照核实
配合自修正机制,能有效提升智能体的可靠性,减少幻觉导致的级联失败。
5. 推理预算与思考过程管理¶
Extended Thinking 提升准确度但也增加成本,需要精心的预算管理。本节介绍四种思考策略(禁用、自适应、预算控制、强制)、复杂度评估方法、思考质量分析,以及会话级的预算跟踪机制。
5.1 推理预算的意义¶
Adaptive Thinking(自适应思考)是 Claude 的现代特性,模型在“思考”中投入 token 进行深度推理,显著提升复杂任务的准确度。但思考本身有成本:
更新: Extended Thinking 的手动模式(
type: "enabled")在 Claude Opus 5、Sonnet 5、Fable 5 与 Opus 4.8/4.7 上已被移除(请求返回 400 错误),在 Opus 4.6 与 Sonnet 4.6 上已弃用但仍可用;Adaptive Thinking(type: "adaptive")是官方推荐方案。反向的边界同样要注意:Sonnet 4.5、Opus 4.5、Haiku 4.5 等更早的模型只有手动模式,传type: "adaptive"会返回 400。完整对照见本节末的附注表。
| 指标 | 说明 | 影响 |
|---|---|---|
| 思考 Token 成本 | 与输出 Token 同价(按输出价计费) | 大量思考会增加成本 |
| 推理时间 | 思考通常需要额外的推理步骤 | 延长响应时间 |
| 质量收益 | 复杂任务可能减少遗漏、重试和错误路径 | 需要用本地 eval 衡量收益 |
| 可预测性 | 思考深度难以精确控制 | budget_tokens 限制 |
5.2 核心概念¶
推理预算的核心数据结构定义如下:
from enum import Enum
from dataclasses import dataclass
from typing import Optional
class ThinkingStrategy(Enum):
"""思考策略"""
DISABLED = "disabled" # 不使用思考
ADAPTIVE = "adaptive" # 自适应(模型决定)
REQUIRED = "required" # 强制思考
BUDGET_BASED = "budget_based" # 基于预算的条件思考
@dataclass
class ReasoningBudget:
"""推理预算配置"""
strategy: ThinkingStrategy
max_thinking_tokens: Optional[int] = None # 单次最大思考 token
max_thinking_per_session: Optional[int] = None # 会话总思考 token
budget_threshold: Optional[float] = None # 成本阈值,超过则不用思考
adaptive_threshold: Optional[float] = None # 自适应触发阈值
task_complexity_threshold: Optional[str] = None # 复杂度阈值
@dataclass
class ThinkingResult:
"""思考结果"""
thinking_tokens: int
thinking_content: str
output_tokens: int
output_content: str
total_cost: float
thinking_ratio: float # 思考 token / 总 token
策略 1: 完全禁用思考¶
最经济的选择,适合简单任务:
class NoThinkingProvider:
"""不使用思考的提供者"""
def __init__(self, config):
self.config = config
self.client = anthropic.Anthropic()
def complete(self, messages, task_type: str = "generic"):
"""不启用 Extended Thinking"""
response = self.client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
messages=messages,
# 注意:没有 thinking 参数
)
return ThinkingResult(
thinking_tokens=0,
thinking_content="",
output_tokens=response.usage.output_tokens,
output_content=response.content[0].text,
total_cost=0,
thinking_ratio=0.0
)
策略 2: 自适应思考¶
让模型自主决定是否思考,适合混合工作负载:
import anthropic
class AdaptiveThinkingProvider:
"""自适应思考提供者"""
def __init__(self, config: ReasoningBudget):
self.config = config
self.client = anthropic.Anthropic()
def complete(self, messages, task_type: str = "generic"):
"""启用自适应思考,模型自主决定"""
# 自适应模式:模型根据任务复杂度自主决定是否思考
# 在 Claude 4.7+,自适应模式自动管理思考深度
response = self.client.messages.create(
model="claude-opus-4-7",
max_tokens=16000,
thinking={
"type": "adaptive", # 推荐方式:模型自主决策思考深度
# Opus 4.8/4.7 上 display 默认为 "omitted"(思考文本为空),
# 需要读取思考内容时必须显式开启
"display": "summarized",
},
messages=messages,
)
# 解析响应中的思考块
thinking_content = ""
output_content = ""
thinking_tokens = 0
output_tokens = 0
for block in response.content:
if block.type == "thinking":
thinking_content = block.thinking
# 从 usage 中获取思考 token 数
elif block.type == "text":
output_content = block.text
# 通过 response.usage 获取准确的 token 统计:
# 思考 token 的明细在 usage.output_tokens_details.thinking_tokens,
# 而 usage.output_tokens 是“已包含思考”的计费权威总数
details = getattr(response.usage, "output_tokens_details", None)
thinking_tokens = getattr(details, "thinking_tokens", 0) if details else 0
output_tokens = response.usage.output_tokens
total_cost = self._calculate_cost(
thinking_tokens,
output_tokens
)
return ThinkingResult(
thinking_tokens=thinking_tokens,
thinking_content=thinking_content,
output_tokens=output_tokens,
output_content=output_content,
total_cost=total_cost,
# output_tokens 已含思考 token,直接取占比即可
thinking_ratio=thinking_tokens / output_tokens
if output_tokens > 0 else 0.0
)
def _calculate_cost(self, thinking_tokens: int, output_tokens: int) -> float:
"""计算输出侧总成本(此示例基于 Opus 4.7 输出价 $25/1M;若改用 Sonnet 4.6 则为 $15/1M,Haiku 4.5 为 $5/1M)"""
# 思考 token 与输出 token 同价,且已计入 output_tokens(计费权威值),
# 不能在输出成本之上再叠加一次思考成本;thinking_tokens 仅用于观测
return output_tokens * 0.025 / 1000 # $25/1M(已含思考)
策略 3: 基于预算的条件思考¶
根据成本和任务复杂度动态决定:
class BudgetBasedThinkingProvider:
"""基于预算的思考提供者"""
def __init__(self, config: ReasoningBudget):
self.config = config
self.client = anthropic.Anthropic()
self.session_thinking_tokens = 0
self.session_cost = 0.0
def complete(self, messages, task_type: str = "generic"):
"""根据预算和任务复杂度决定是否思考"""
# 评估任务复杂度
complexity = self._assess_complexity(messages, task_type)
# 检查成本预算
estimated_thinking_cost = self._estimate_cost(complexity)
can_afford = (
self.session_cost + estimated_thinking_cost <
self.config.budget_threshold
)
# 检查 token 预算
can_afford_tokens = (
self.session_thinking_tokens +
(self.config.max_thinking_tokens or 10000) <
(self.config.max_thinking_per_session or 100000)
)
should_think = (
complexity in ["hard", "very_hard"] and
can_afford and
can_afford_tokens
)
if should_think:
return self._complete_with_thinking(messages)
else:
return self._complete_without_thinking(messages)
def _assess_complexity(self, messages: list, task_type: str) -> str:
"""评估任务复杂度"""
# 基于消息长度、任务类型等因素
total_tokens = sum(
len(msg.get("content", "").split()) * 1.3
for msg in messages
)
type_complexity = {
"reasoning": "hard",
"coding": "hard",
"analysis": "medium",
"summarization": "easy",
"generic": "medium",
}
base_complexity = type_complexity.get(task_type, "medium")
if total_tokens > 5000:
return "very_hard" if base_complexity == "hard" else base_complexity
elif total_tokens > 2000:
return base_complexity
else:
return "easy"
def _estimate_cost(self, complexity: str) -> float:
"""估算思考成本"""
complexity_to_tokens = {
"easy": 2000,
"medium": 5000,
"hard": 10000,
"very_hard": 15000,
}
tokens = complexity_to_tokens.get(complexity, 5000)
return tokens * 0.025 / 1000 # 思考 token 成本(与 Opus 4.7 输出同价 $25/1M)
def _complete_with_thinking(self, messages):
"""带思考的完成"""
# 注:在 Claude 4.7+ 使用 type:"adaptive",旧模型可用 type:"enabled" with budget_tokens
response = self.client.messages.create(
model="claude-opus-4-7", # Opus 4.6+ 推荐使用 adaptive 模式
max_tokens=16000,
thinking={
"type": "adaptive", # 推荐改用 adaptive 而非已弃用的 enabled
"display": "summarized", # Opus 4.8/4.7 默认 omitted,需显式开启
},
messages=messages,
)
# 解析思考块和输出
thinking_content = ""
output_content = ""
for block in response.content:
if block.type == "thinking":
thinking_content = block.thinking
elif block.type == "text":
output_content = block.text
details = getattr(response.usage, "output_tokens_details", None)
thinking_tokens = getattr(details, "thinking_tokens", 0) if details else 0
output_tokens = response.usage.output_tokens # 已含思考 token
# 关键:更新会话级预算跟踪,否则 complete() 的预算闸门永远是 $0
total_cost = output_tokens * 0.025 / 1000 # Opus 4.7 输出价,已含思考
self.session_thinking_tokens += thinking_tokens
self.session_cost += total_cost
return ThinkingResult(
thinking_tokens=thinking_tokens,
thinking_content=thinking_content,
output_tokens=output_tokens,
output_content=output_content,
total_cost=total_cost,
thinking_ratio=thinking_tokens / output_tokens
if output_tokens > 0 else 0.0,
)
def _complete_without_thinking(self, messages):
"""不使用思考的完成"""
response = self.client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
messages=messages,
)
output_content = ""
for block in response.content:
if block.type == "text":
output_content = block.text
total_cost = response.usage.output_tokens * 0.015 / 1000 # Sonnet 4.6 输出价
self.session_cost += total_cost
return ThinkingResult(
thinking_tokens=0,
thinking_content="",
output_tokens=response.usage.output_tokens,
output_content=output_content,
total_cost=total_cost,
thinking_ratio=0.0,
)
策略 4: 强制思考¶
某些任务(如代码审查、安全决策)必须激活思考:
class RequiredThinkingProvider:
"""强制思考提供者"""
CRITICAL_TASK_TYPES = {
"security_decision",
"financial_transaction",
"code_review",
"medical_advice",
}
def __init__(self, config: ReasoningBudget):
self.config = config
self.client = anthropic.Anthropic()
def complete(self, messages, task_type: str = "generic"):
"""对关键任务强制启用思考"""
if task_type not in self.CRITICAL_TASK_TYPES:
raise ValueError(
f"任务类型 {task_type} 不需要强制思考"
)
# 构建强制思考的提示
enhanced_messages = self._enhance_for_deep_thinking(messages)
response = self.client.messages.create(
model="claude-opus-4-7",
max_tokens=16000,
thinking={
"type": "adaptive", # adaptive 自动管理思考深度,无需显式 budget_tokens
# 必须显式开启 summarized:Opus 4.8/4.7 默认 omitted,
# 否则 thinking 字段恒为空,下方的深度校验会永远失败
"display": "summarized",
},
messages=enhanced_messages,
)
# ... 验证思考深度
thinking_content = ""
for block in response.content:
if block.type == "thinking":
thinking_content = block.thinking
break
if not thinking_content or len(thinking_content) < 500:
raise ValueError(
"思考内容不足,可能是关键任务的思考不够深入"
)
# 解析输出内容
output_content = ""
for block in response.content:
if block.type == "text":
output_content = block.text
break
details = getattr(response.usage, "output_tokens_details", None)
thinking_tokens = getattr(details, "thinking_tokens", 0) if details else 0
output_tokens = response.usage.output_tokens # 已含思考 token
return ThinkingResult(
thinking_tokens=thinking_tokens,
thinking_content=thinking_content,
output_tokens=output_tokens,
output_content=output_content,
total_cost=output_tokens * 0.025 / 1000, # Opus 4.7 输出价,已含思考
thinking_ratio=thinking_tokens / output_tokens
if output_tokens > 0 else 0.0,
)
def _enhance_for_deep_thinking(self, messages):
"""增强消息以促进更深的思考"""
enhanced = messages.copy()
enhanced[-1]["content"] += (
"\n\n请花时间仔细思考这个问题的所有方面,"
"包括潜在的风险和边界情况。"
)
return enhanced
思考结果分析¶
对思考过程进行质量分析的实现方式如下:
class ThinkingAnalyzer:
"""思考过程分析"""
def analyze_thinking_quality(self, result: ThinkingResult) -> dict:
"""分析思考质量和成本效率"""
analysis = {
"thinking_ratio": result.thinking_ratio,
"cost_efficiency": self._calculate_efficiency(result),
"thinking_depth": self._assess_depth(result.thinking_content),
"quality_indicators": self._extract_quality_indicators(result),
}
return analysis
def _calculate_efficiency(self, result: ThinkingResult) -> float:
"""计算成本效率(质量/成本)"""
if result.total_cost == 0:
return 0
# 假设有一个质量评分
quality_score = self._estimate_quality(result.output_content)
return quality_score / result.total_cost
def _assess_depth(self, thinking_content: str) -> str:
"""评估思考深度"""
if not thinking_content:
return "none"
if len(thinking_content) < 500:
return "shallow"
elif len(thinking_content) < 2000:
return "moderate"
else:
return "deep"
def _extract_quality_indicators(self, result: ThinkingResult) -> dict:
"""从思考过程中提取质量指标"""
thinking = result.thinking_content
indicators = {
"considers_alternatives": "另一种" in thinking or "也可以" in thinking,
"identifies_constraints": "限制" in thinking or "约束" in thinking,
"checks_edge_cases": "边界" in thinking or "特殊情况" in thinking,
"shows_uncertainty": "可能" in thinking or "不确定" in thinking,
}
return indicators
def _estimate_quality(self, output_content: str) -> float:
"""粗略估计输出质量(0-1,示意启发式;生产中应替换为本地 eval)"""
if not output_content:
return 0.0
score = 0.4
if len(output_content) > 200:
score += 0.2 # 有一定篇幅
if any(kw in output_content for kw in ("因为", "所以", "首先", "步骤")):
score += 0.2 # 有结构化推理痕迹
if "```" in output_content or "|" in output_content:
score += 0.2 # 含代码/表格等结构化产出
return min(score, 1.0)
def should_retry_with_more_thinking(self, result: ThinkingResult) -> bool:
"""判断是否应该用更多思考重试"""
# 如果输出质量低且思考比例低,可以重试
quality = self._estimate_quality(result.output_content)
if quality < 0.5 and result.thinking_ratio < 0.3:
return True
return False
推理预算管理器¶
推理预算管理器的完整实现如下:
class ReasoningBudgetManager:
"""推理预算管理"""
def __init__(self, config: ReasoningBudget):
self.config = config
self.session_stats = {
"total_thinking_tokens": 0,
"total_cost": 0.0,
"request_count": 0,
"avg_thinking_ratio": 0.0,
}
def should_enable_thinking(
self,
task_type: str,
estimated_complexity: str
) -> bool:
"""决定是否启用思考"""
if self.config.strategy == ThinkingStrategy.DISABLED:
return False
elif self.config.strategy == ThinkingStrategy.REQUIRED:
return True
elif self.config.strategy == ThinkingStrategy.ADAPTIVE:
return estimated_complexity in ["hard", "very_hard"]
elif self.config.strategy == ThinkingStrategy.BUDGET_BASED:
# 检查成本预算
can_afford = (
self.session_stats["total_cost"] < self.config.budget_threshold
)
return can_afford and estimated_complexity != "easy"
return False
def record_thinking_usage(self, result: ThinkingResult):
"""记录思考使用情况"""
self.session_stats["total_thinking_tokens"] += result.thinking_tokens
self.session_stats["total_cost"] += result.total_cost
self.session_stats["request_count"] += 1
# 更新平均思考比例
prev_avg = self.session_stats["avg_thinking_ratio"]
n = self.session_stats["request_count"]
self.session_stats["avg_thinking_ratio"] = (
(prev_avg * (n - 1) + result.thinking_ratio) / n
)
def get_budget_status(self) -> dict:
"""获取预算使用状态"""
return {
"used": self.session_stats["total_cost"],
"limit": self.config.budget_threshold,
"remaining": (
self.config.budget_threshold -
self.session_stats["total_cost"]
),
"requests": self.session_stats["request_count"],
"avg_thinking_ratio": self.session_stats["avg_thinking_ratio"],
}
使用示例¶
推理预算管理器的使用示例如下:
# 配置基于预算的思考
config = ReasoningBudget(
strategy=ThinkingStrategy.BUDGET_BASED,
max_thinking_tokens=15000,
max_thinking_per_session=100000,
budget_threshold=0.50, # 最多 $0.50
task_complexity_threshold="medium"
)
manager = ReasoningBudgetManager(config)
provider = BudgetBasedThinkingProvider(config)
# 处理任务
result = provider.complete(messages, task_type="reasoning")
# 分析结果
analyzer = ThinkingAnalyzer()
analysis = analyzer.analyze_thinking_quality(result)
print(f"思考深度: {analysis['thinking_depth']}")
print(f"成本效率: {analysis['cost_efficiency']:.4f}")
# 记录使用
manager.record_thinking_usage(result)
status = manager.get_budget_status()
print(f"预算使用: ${status['used']:.4f} / ${status['limit']:.4f}")
总结¶
推理预算管理通过:
- 多种策略 (禁用、自适应、预算、强制)满足不同需求
- 动态复杂度评估 决定思考投入
- 成本和质量平衡 优化成本效益
- 会话级预算跟踪 防止失控支出
这是生产智能体系统必不可少的能力,尤其是当扩展思考成为标配时。
5.3 附注:Extended Thinking 的模型差异¶
Claude 官方对手动 Extended Thinking 配置(thinking: {type: "enabled", budget_tokens: N})按模型区分处理:
| 模型 | 状态 |
|---|---|
| Claude Opus 5、Sonnet 5、Fable 5(当前一代) | ❌ 不支持,返回 400 错误,只提供 Adaptive Thinking |
| Claude Opus 4.8/4.7 | ❌ 不支持,返回 400 错误 |
| Claude Opus 4.6、Sonnet 4.6 | ⚠️ 已弃用但功能正常(计划迁移) |
| Claude Opus 4.5、Sonnet 4.5、Haiku 4.5 等早期模型 | ✅ 仍支持,且是唯一模式(传 type: "adaptive" 反而返回 400) |
迁移指南: 对支持 adaptive thinking 的模型,优先使用 thinking: {type: "adaptive"}。通过 output_config.effort 参数控制思考深度(max, xhigh, high, medium, low),而不是依赖固定 budget_tokens。
6. 实战:实现 MiniHarness 输出治理层¶
前面章节的理论知识需要在生产系统中整合落地。本节展示模型抽象设计如何支持多个 LLM 提供者、响应解析的具体实现、质量门控的分层验证,以及各组件的集成使用示例。
6.1 设计目标¶
构建生产级的 MiniHarness 系统,整合模型抽象、输出解析、质量门控和幻觉检测。完整代码位于 lab/mini_harness/models/。
6.2 系统架构¶
User Request → [模型抽象层] → [LLM API] → [响应解析] → [幻觉检测] → [质量门控] → [工具执行] → Response
6.3 模型抽象设计¶
核心思想:“接口统一、实现可插”,支持多个 LLM 提供者。
关键设计点:
- BaseProvider 抽象:定义统一接口
complete()、stream()、complete_with_tools(tools)- 其中工具参数必需(非可选) - 多 Provider 支持:Claude(Anthropic SDK)、OpenAI 兼容(DeepSeek、Qwen、Ollama 等)
- 工具转换层:自动将 MiniHarness 工具格式转换为各 Provider 的 API 格式
- 熔断器三态:closed(正常)→ open(故障)→ half-open(恢复测试)
class BaseProvider(ABC):
def complete(self, messages: List[Message],
tools: Optional[List[Dict]] = None) -> ProviderResponse:
pass
def stream(self, messages: List[Message],
tools: Optional[List[Dict]] = None) -> Generator[str, None, None]:
pass
class ClaudeProvider(BaseProvider):
def complete(self, messages, tools=None):
# 直接使用 Anthropic SDK,tool_use 是一等内容块
response = self.client.messages.create(...)
return ProviderResponse(content=..., tool_calls=[...])
class OpenAIProvider(BaseProvider):
def complete(self, messages, tools=None):
# 转换工具格式:MiniHarness → OpenAI function calling
openai_tools = self._convert_tools(tools)
response = self.client.chat.completions.create(...)
return ProviderResponse(content=..., tool_calls=[...])
熔断器实现 (半开状态需要多次成功才关闭):
class CircuitBreaker:
def is_available(self) -> bool:
if self.state == "closed":
return True
if self.state == "open":
if time.time() - self.last_failure_time > self.reset_timeout:
self.state = "half-open"
return True
return self.state == "half-open"
故障转移:
class ModelSelectionEngine:
def select_model(self) -> BaseProvider:
for config in [self.primary] + self.fallback_chain:
if self.breakers[config.model_id].is_available():
return create_provider(config)
raise Exception("所有模型不可用")
完整代码见:lab/mini_harness/models/provider.py
6.4 响应解析设计¶
核心思想:“结构化输出”,统一处理多种内容块类型。
设计要点:
- 内容块类型:
TextBlock(文本)、ToolUseBlock(工具调用)、ThinkingBlock(思考过程) - 类型安全:使用 dataclass 和 Union 类型标注
- 便利方法:
text_content()合并所有文本、tool_calls()提取工具调用
@dataclass
class TextBlock:
type: str = "text"
text: str = ""
@dataclass
class ToolUseBlock:
type: str = "tool_use"
id: str = ""
name: str = ""
input: dict = None
@dataclass
class ParsedMessage:
content_blocks: List[ContentBlock]
stop_reason: str
tokens_used: int
def tool_calls(self) -> List[ToolUseBlock]:
return [b for b in self.content_blocks
if isinstance(b, ToolUseBlock)]
解析逻辑:遍历原始响应块,按类型创建相应对象,JSON 字符串自动反序列化为 dict。
完整代码见:lab/mini_harness/models/parser.py
6.5 质量门控设计¶
核心思想:“执行前验证”,三层防线防止异常工具调用。
质量门控层次:
graph TD
A["<b>ToolUseBlock</b>"]
B["<b>幻觉检测</b><br/>工具名是否存在?参数范围合理?"]
C["<b>结构验证</b><br/>ID、名称字段完整?"]
D["<b>Schema 验证</b><br/>参数符合 Pydantic schema?"]
E["<b>执行</b><br/>或<br/><b>拒绝 + 建议</b>"]
A -->|检查| B
B -->|验证| C
C -->|校验| D
D -->|决策| E
style A fill:#f9f7ed
style B fill:#ebe3d9
style C fill:#ddd0c4
style D fill:#d0c4b8
style E fill:#c4b8ac
关键类:
class QualityToolRegistry:
def register(self, name: str, handler, schema):
self.tools[name] = {"handler": handler, "schema": schema, "enabled": True}
def is_tool_available(self, name: str) -> bool:
return name in self.tools and self.tools[name]["enabled"]
class QualityGate:
def validate_tool_call(self, tool_call: ToolUseBlock) -> ValidationReport:
# 1. 检查 ID、名称
# 2. 检查工具存在性 + 可用性
# 3. 使用 pydantic 验证参数
# 返回:PASS 或 FAIL + errors + suggestion
...
幻觉检测(模糊匹配建议):
class HallucinationDetector:
def detect(self, tool_call: ToolUseBlock) -> List[HallucinationResult]:
if not self.registry.is_tool_available(tool_call.name):
suggestions = get_close_matches(
tool_call.name, available_tools, n=1, cutoff=0.6)
return [HallucinationResult(
is_hallucination=True,
confidence=0.9,
suggestion=f"您想要 '{suggestions[0]}' 吗?"
)]
完整代码见:lab/mini_harness/models/quality.py
6.6 集成示例¶
以下示例展示了如何将模型提供者、质量门控和工具注册表组合使用,完成一次带有输出校验的模型调用流程。
from pydantic import BaseModel
from mini_harness.models.parser import ToolUseBlock
from mini_harness.models.provider import (
ModelConfig,
ModelProviderType,
ProviderMessage,
create_provider,
)
from mini_harness.models.quality import QualityToolRegistry, QualityGate, ValidationResult
# 配置模型
config = ModelConfig(
provider=ModelProviderType.CLAUDE,
model_id="claude-opus-4-7",
api_key="your-api-key"
)
# 创建工具注册表
registry = QualityToolRegistry()
class SearchInput(BaseModel):
query: str
max_results: int = 10
def search_handler(query: str, max_results: int = 10):
return {"results": [...]}
registry.register("search", search_handler, SearchInput)
# 调用流程
provider = create_provider(config)
messages = [ProviderMessage("user", "搜索 Harness Engineering")]
tools = [{
"name": "search",
"description": "Search documents",
"input_schema": SearchInput.model_json_schema(),
}]
response = provider.complete(messages, tools=tools)
for call in response.tool_calls:
tool_call = ToolUseBlock(
id=call["id"],
name=call["name"],
input=call.get("arguments", {}),
)
validation = QualityGate(registry).validate_tool_call(tool_call)
if validation.result == ValidationResult.PASS:
handler = registry.tools[tool_call.name]["handler"]
result = handler(**tool_call.input)
6.7 关键特性总结¶
| 功能 | 设计模式 | 益处 |
|---|---|---|
| 模型抽象 | 策略模式 + 工厂模式 | 支持多 LLM,轻松切换 |
| 工具转换 | 适配器模式 | 屏蔽 API 差异 |
| 熔断器 | 状态机 | 自动故障转移 |
| 响应解析 | dataclass + Union | 类型安全、易于扩展 |
| 质量门控 | 装饰器链 | 分层验证,关注点分离 |
| 幻觉检测 | 相似度 + 范围检查 | 多角度识别模型错误 |
完整实现和更多示例见 lab/mini_harness/ 目录。
7. 本章小结¶
核心议题
模型集成是智能体系统与 LLM 能力之间的关键桥梁。本章系统地探讨了如何构建一个健壮、灵活、高效的模型集成与输出治理层,使智能体能够安全地调用 LLM,并可靠地执行工具调用。
7.1 模型抽象层设计¶
关键成果:
- Provider 接口:定义了统一的模型调用接口(complete、stream、estimate_tokens、validate_config)
- 多模型 vs 单模型权衡:
- OpenClaw(多模型):适合成本优化、供应商多元化
- Claude Code(单模型绑定):适合深度优化和特性集成
- 故障转移机制:熔断器 + 备用链路,透明切换,无感于上层应用
架构图(同图 7-1):
详见第 7.1 节的模型抽象层架构设计。
7.2 结构化输出解析与校验¶
关键成果:
- 类型系统:TextBlock / ToolUseBlock / ThinkingBlock,提供类型安全的消息表示
- 双路径解析:
- 非流式:一次性处理完整响应
- 流式:增量解析,事件驱动,支持实时反馈
- Pydantic 校验:在解析后立即进行参数验证
解析管道流程(同图 7-2):
输出解析的关键步骤包括:解析原始 API 响应、构建类型安全的消息对象、验证工具调用参数。完整流程请参考第 7.2 节。
7.3 输出质量门控与过滤¶
关键成果:
- 多层门控(6 层):
- 格式检查:结构完整性
- 工具存在性:注册表匹配
- 参数类型与范围:Pydantic 校验
- 业务规则:API 频率、文件大小等
- 权限检查:用户授权、敏感操作
- 注入防护:危险模式检测
- 一票否决制:任何一层失败即阻止执行
门控流程图(同图 7-3):
详见第 7.3 节的质量门控多层验证流程设计。
7.4 幻觉检测与工具调用验证¶
关键成果:
- 三层幻觉防线:
- Layer 1:工具名幻觉(高置信度,可纠正)
- Layer 2:参数幻觉(范围和格式)
- Layer 3:事实幻觉(知识库对照)
- 自修正机制:不是简单拒绝,而是生成纠正建议,让模型重新尝试
- 危害量化:
- 工具幻觉 → 执行失败 + token 浪费
- 参数幻觉 → 安全风险 + 数据泄露
- 事实幻觉 → 级联失败 + 用户困惑
检测流程图(同图 7-4):
详见第 7.4 节的幻觉检测三层防线设计。
7.5 推理预算与思考过程管理¶
关键成果:
- 四种思考策略:
- DISABLED:成本最低,简单任务
- ADAPTIVE:模型自主决策
- BUDGET_BASED:动态权衡成本与质量
- REQUIRED:强制深思考(关键任务)
- 成本模型:思考 token 与输出 token 同价(按输出价计费),大量思考仍会显著增加成本,需精细化管理
- 质量分析:
- 思考比例(thinking_ratio)
- 思考深度(shallow/moderate/deep)
- 成本效率(quality/cost)
- 质量指标(考虑替代方案、边界情况、约束识别)
预算决策树:
graph TD
A["Task Arrives"] --> B{Strategy Type?}
B -->|DISABLED| C["No Thinking"]
B -->|REQUIRED| D["Force Thinking"]
B -->|BUDGET_BASED| E["<b>Assessment</b><br/>Complexity"]
B -->|ADAPTIVE| F["Let model decide"]
E --> G{Remaining Budget<br/>OK?}
G -->|No| H["No Thinking"]
G -->|Yes| I{Remaining Tokens<br/>OK?}
I -->|No| H
I -->|Yes| J{Complexity<br/>Medium+?}
J -->|No| K["No Thinking"]
J -->|Yes| L["Enable Thinking"]
style A fill:#e8f4f8,stroke:#4a90a4,stroke-width:2px,color:#000000
style B fill:#fff4e8,stroke:#a49044,stroke-width:2px,color:#000000
style C fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px,color:#000000
style D fill:#fff8c4,stroke:#ffb74d,stroke-width:2px,color:#000000
style E fill:#fff4e8,stroke:#a49044,stroke-width:2px,color:#000000
style F fill:#e8f4f8,stroke:#4a90a4,stroke-width:2px,color:#000000
style G fill:#fff4e8,stroke:#a49044,stroke-width:2px,color:#000000
style I fill:#fff4e8,stroke:#a49044,stroke-width:2px,color:#000000
style J fill:#fff4e8,stroke:#a49044,stroke-width:2px,color:#000000
style L fill:#fff8c4,stroke:#ffb74d,stroke-width:2px,color:#000000
7.6 实战:MiniHarness 输出治理层¶
关键成果:
- 完整集成:从模型选择 → 响应解析 → 幻觉检测 → 质量门控 → 工具执行
- 关键类:
- ModelSelectionEngine:故障转移
- ResponseParser / StreamingParser:响应解析
- HallucinationDetector:三层检测
- QualityGate:多层验证
- MiniHarness:主协调类
- 可扩展性:
- 支持新模型接入(实现 Provider 接口)
- 支持新工具注册(ToolRegistry)
- 支持自定义验证规则(继承 Validator)
完整流程图:
graph TD
A["User Query"] --> B["ModelSelectionEngine"]
B --> C["Select Viable Provider"]
C --> D["Call LLM API"]
D --> E["<b>Parse Response</b><br/>(TextBlock/ToolUseBlock/<br/>ThinkingBlock)"]
E --> F["For Each Tool Call"]
F --> G["<b>Hallucination Detection</b><br/>Tool Name / Parameter / Fact"]
G --> H["<b>Quality Gate</b><br/>Format / Type / Range /<br/>Business / Permission"]
H --> I{Decision}
I -->|Valid| J["Execute Tool"]
I -->|Invalid| K["Report Error"]
J --> L["Return Results to User"]
K --> L
style A fill:#e8f4f8,stroke:#4a90a4,stroke-width:2px,color:#000000
style B fill:#e8f4f8,stroke:#4a90a4,stroke-width:2px,color:#000000
style C fill:#fff4e8,stroke:#a49044,stroke-width:2px,color:#000000
style D fill:#e8f4f8,stroke:#4a90a4,stroke-width:2px,color:#000000
style E fill:#fff4e8,stroke:#a49044,stroke-width:2px,color:#000000
style F fill:#fff4e8,stroke:#a49044,stroke-width:2px,color:#000000
style G fill:#ffcccc,stroke:#c62828,stroke-width:2px,color:#000000
style H fill:#ffcccc,stroke:#c62828,stroke-width:2px,color:#000000
style I fill:#fff4e8,stroke:#a49044,stroke-width:2px,color:#000000
style J fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px,color:#000000
style K fill:#ffcccc,stroke:#c62828,stroke-width:2px,color:#000000
style L fill:#e8f4f8,stroke:#4a90a4,stroke-width:2px,color:#000000
7.6.1 核心原则总结¶
| 原则 | 含义 | 实现 |
|---|---|---|
| 防御深度 | 多层防护,缺一不可 | 6 层门控 + 3 层幻觉检测 |
| 故障隔离 | 一个失败不影响全局 | 熔断器 + 备用链路 |
| 成本意识 | 推理有成本,需预算管理 | 复杂度评估 + 预算跟踪 |
| 用户友好 | 失败时提供纠正建议 | 自修正机制,不是简单拒绝 |
| 可观测性 | 完整的数据流跟踪 | token 计数、幻觉日志、门控报告 |
| 灵活性 | 支持多模型和多策略 | 抽象接口、可配置规则 |
7.6.2 与参考系统的对比¶
OpenClaw (开源智能体框架):
- 优势:多模型支持、生态丰富
- 劣势:通用性强,某些功能定制困难
- 本章学习:模型选择、故障转移的模式
Claude Code (官方智能体工具):
- 优势:Claude 特性深度集成(Adaptive Thinking)、高效的工具调用
- 劣势:受限于单一模型
- 本章学习:推理预算、消息规范化的最佳实践
MiniHarness (本章实现):
- 目标:综合两者优势,提供教学性的、完整的参考实现
- 特点:模块化、可扩展、注重治理
7.6.3 实战要点¶
- 从模型抽象开始:不要硬编码 API 调用,使用 Provider 接口
- 解析要完整:支持流式和非流式,处理所有内容块类型
- 验证要严格:多层检查,宁可拒绝可疑调用,也不要执行失败操作
- 幻觉要检测:工具名、参数、事实三层都要覆盖
- 预算要跟踪:定期检查 token 使用和成本,防止失控
7.6.4 进阶方向¶
- 更细的复杂度评估:基于任务描述、参数数量、历史成功率等
- 动态调整门控严格度:根据用户权限等级调整
- 跨轮对话优化:在多轮智能体循环中智能管理上下文和推理
- 幻觉频率监控:识别特定任务类型中的高幻觉率,主动提升思考预算
- 工具执行结果反馈:让模型从工具执行失败中学习,改进提示
7.6.5 建议的阅读路径¶
初学者:7.1 → 7.2 → 7.3 → 7.6(快速上手)
系统开发者:7.1 → 7.3 → 7.4 → 7.5 → 7.6(全流程理解)
性能优化者:7.4 → 7.5(幻觉和成本)
架构师:7.1 + 7.3(抽象和门控)
7.6.6 章总结语¶
模型集成与输出治理是智能体系统的 可靠性基石。通过合理的抽象、严格的验证、智能的幻觉检测和成本预算管理,我们可以将 LLM 这种强大但不确定的工具,转化为稳定、可控的智能体能力。
本章的设计和代码示例可直接应用于生产系统,也为后续章节(多智能体协调、长期规划、学习与记忆)奠定了坚实的基础。