1. 项目概述:构建能调用工具的LLM Agent
去年我在一个客户现场第一次看到GPT-4成功调用计算器完成数学题时,那种震撼感至今难忘。传统语言模型就像个只会纸上谈兵的理论家,而具备工具调用能力的Agent则瞬间进化为能实操落地的实干家。这次我们要实现的,正是这样一个能自主决策何时、如何调用外部工具的智能体系统。
这个项目的核心价值在于突破纯文本生成的限制。想象一下,当用户问"纽约现在几点",你的Agent能自动调用世界时钟API;当需要复杂计算时,它能无缝切换至计算引擎;甚至可以根据需求组合多个工具形成工作流。这种能力将LLM从"聊天机器人"升级为真正的"数字助理"。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 架构设计:从原理到实现
2.1 核心组件拆解
一个完整的Tool-using Agent通常包含三大模块:
- 意图识别引擎:分析用户query判断是否需要调用工具
- 工具路由系统:匹配最适合当前任务的工具
- 执行监控层:处理工具调用结果并决定后续动作
go复制type Agent struct {
LLM *LanguageModel // 基础语言模型
ToolRegistry []Tool // 注册的工具集合
MaxRetries int // 最大重试次数
}
type Tool interface {
Name() string
Description() string
Execute(input string) (string, error)
}
2.2 工具调用流程设计
典型的工作流程如下:
- 用户输入原始query
- LLM判断是否需要工具调用(JSON格式响应)
- 系统定位并执行对应工具
- 将工具输出重新注入LLM上下文
- 生成最终响应给用户
bash复制# 示例调用链
curl -X POST http://agent-service/v1/chat \
-d '{
"message": "计算3的5次方",
"tools": ["calculator"]
}'
3. 关键实现细节
3.1 工具描述规范
工具的描述质量直接影响LLM的调用准确性。我们采用OpenAI推荐的标准化格式:
json复制{
"name": "world_clock",
"description": "获取指定时区的当前时间。时区格式需符合IANA标准(如Asia/Shanghai)",
"parameters": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "IANA时区标识符"
}
}
}
}
重要提示:description字段要包含精确的输入输出示例,避免模糊表述如"处理时间相关查询"
3.2 混合调度策略
在实际场景中,我们采用分层决策机制:
- 第一层:硬编码规则匹配(如检测到"计算"关键词直接触发计算器)
- 第二层:嵌入向量相似度匹配(工具描述与query的cosine相似度)
- 第三层:LLM自主决策(当上述方法无法确定时)
go复制func (a *Agent) SelectTool(query string) (Tool, error) {
// 第一层:规则匹配
if containsMathKeywords(query) {
return a.GetTool("calculator"), nil
}
// 第二层:向量匹配
if bestMatch := a.vectorMatch(query); bestMatch != nil {
return bestMatch, nil
}
// 第三层:LLM决策
return a.llmDecideTool(query)
}
4. 实战:构建计算器工具
4.1 Shell实现的数学引擎
对于简单计算需求,可以直接利用shell的bc命令:
bash复制#!/bin/bash
# 安全验证函数
sanitize_input() {
# 移除所有非数字和运算符字符
echo "$1" | tr -cd '0-9.+-*/^%() '
}
# 主处理逻辑
expression=$(sanitize_input "$1")
result=$(echo "scale=2; $expression" | bc -l 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: 计算失败 - $result"
exit 1
fi
echo $result
4.2 Go语言封装
通过Go的exec包调用shell并添加超时控制:
go复制type Calculator struct{}
func (c Calculator) Execute(input string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "/bin/bash", "calculator.sh", input)
output, err := cmd.CombinedOutput()
if ctx.Err() == context.DeadlineExceeded {
return "", errors.New("计算超时")
}
return string(output), err
}
5. 高级技巧与避坑指南
5.1 工具注册最佳实践
- 命名冲突预防:工具名称添加命名空间前缀(如"math_")
- 版本控制:在描述中注明工具版本号
- 冷启动方案:为每个工具准备3-5个典型调用示例
go复制func registerTools() []Tool {
return []Tool{
&Calculator{metadata: ToolMeta{
Name: "math_calculator_v1",
Description: "执行基础算术运算(加减乘除、指数、取模)。示例输入:'3*(5+2)^2'",
Version: "1.0.2",
}},
// 其他工具...
}
}
5.2 常见故障排查
问题1:LLM频繁错误调用工具
- 解决方案:在工具描述中添加否定示例(如"不要用于日期计算")
问题2:工具执行超时
- 优化方案:实现执行进度心跳检测
go复制func withHeartbeat(ctx context.Context, interval time.Duration, fn func()) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
go func() {
for {
select {
case <-ticker.C:
log.Println("工具执行中...")
case <-ctx.Done():
return
}
}
}()
fn()
}
问题3:敏感信息泄露
- 防护措施:在工具接口层实现数据脱敏
go复制func sanitizeOutput(output string) string {
// 移除信用卡号等敏感信息
re := regexp.MustCompile(`\b(?:\d[ -]*?){13,16}\b`)
return re.ReplaceAllString(output, "[REDACTED]")
}
6. 性能优化策略
6.1 工具预热机制
对于启动耗时的工具(如加载大型模型的Python脚本),采用守护进程模式:
bash复制# 使用socat创建持久化服务
socat TCP-LISTEN:8080,fork,reuseaddr EXEC:"python3 tool_wrapper.py"
6.2 批量处理优化
当检测到连续工具调用时,自动合并请求:
go复制func (a *Agent) batchRequests(queries []string) []Result {
// 按工具类型分组
batchMap := make(map[string][]string)
for _, q := range queries {
tool := a.SelectTool(q)
batchMap[tool.Name()] = append(batchMap[tool.Name()], q)
}
// 并行处理各批次
var wg sync.WaitGroup
results := make([]Result, len(queries))
for toolName, batch := range batchMap {
wg.Add(1)
go func(name string, inputs []string) {
defer wg.Done()
tool := a.GetTool(name)
// ...批量处理逻辑
}(toolName, batch)
}
wg.Wait()
return results
}
7. 安全防护方案
7.1 输入验证框架
建立多层防御体系:
- 语法层:正则表达式白名单
- 语义层:LLM二次验证
- 执行层:沙箱环境隔离
go复制type SafetyChecker interface {
ValidateSyntax(input string) bool
ValidateSemantics(input string) bool
}
type CalculatorSafety struct{}
func (c CalculatorSafety) ValidateSyntax(input string) bool {
return regexp.MustCompile(`^[0-9+\-*/%^.() ]+$`).MatchString(input)
}
func (c CalculatorSafety) ValidateSemantics(input string) bool {
// 使用微型LLM判断是否为合法数学表达式
// ...
}
7.2 资源隔离方案
使用Docker实现工具级隔离:
bash复制# 工具容器启动脚本
docker run --rm \
--memory 100M \
--cpus 0.5 \
--network none \
-v $(pwd)/tools:/tools \
tool-container:latest \
/tools/calculator.sh "$input"
8. 扩展方向
8.1 动态工具加载
实现无需重启的热更新能力:
go复制func (a *Agent) LoadTool(pluginPath string) error {
// 使用Go插件系统
plug, err := plugin.Open(pluginPath)
if err != nil {
return err
}
symTool, err := plug.Lookup("Tool")
if err != nil {
return err
}
tool, ok := symTool.(Tool)
if !ok {
return errors.New("invalid tool interface")
}
a.ToolRegistry = append(a.ToolRegistry, tool)
return nil
}
8.2 工具组合工作流
通过DSL定义复杂任务流:
yaml复制name: 旅行规划
steps:
- tool: flight_search
params:
origin: "{{user_input.departure}}"
destination: "{{user_input.arrival}}"
- tool: weather_forecast
params:
location: "{{step1.output.destination}}"
days: 3
- tool: hotel_search
params:
location: "{{step1.output.destination}}"
check_in: "{{user_input.date}}"
在实现LLM Agent调用工具的过程中,最深的体会是"确定性"与"灵活性"的平衡艺术。工具调用必须保持严格的输入输出契约,而LLM的决策又需要足够的容错空间。经过多个项目的迭代,我发现采用"三层验证+渐进式回退"的策略最为可靠——先用规则引擎处理明确场景,再用向量匹配处理相似场景,最后才交给LLM做开放决策。
