AI Agent 工具参数传递:从原理到代码实现
工具参数传递为何容易出错
AI Agent 的核心能力在于"动手"——调用计算器查汇率、操作数据库跑查询、控制浏览器填表单。但大模型生成的参数经常对不上工具的输入格式:该传数字的地方给了字符串,必填字段漏了,或者干脆调了个不存在的工具。
问题根源在于 LLM 的输出是文本,而工具需要结构化数据。中间这层转换做得好不好,直接决定 Agent 是能用还是摆设。
两种主流实现模式
直接调用模式
最简单的做法是让模型输出 JSON,直接解析后调用对应函数。OpenAI 的 Function Calling 走的就是这条路。
class DirectAgent:
def __init__(self, llm, tools):
self.llm = llm
self.tools = {t.name: t for t in tools}
def run(self, task):
# 构造带工具描述的提示
tool_desc = "\n".join([f"- {name}: {t.description}"
for name, t in self.tools.items()])
prompt = f"""任务:{task}
可用工具:
{tool_desc}
请输出 JSON:{{"tool": "工具名", "params": {{参数}}}}"""
response = self.llm.generate(prompt)
call = json.loads(response)
# 执行调用
tool = self.tools[call["tool"]]
return tool.execute(**call["params"])优点是快,单次交互就能完成。缺点是容错差,模型一旦输出格式不对就崩,也没法处理多步骤任务。
ReAct 循环模式
ReAct(Reasoning + Acting)把任务拆成"思考→行动→观察"的循环,每轮只走一步,用观察结果修正下一步。
class ReActAgent:
def __init__(self, llm, tools, max_iter=10):
self.llm = llm
self.tools = {t.name: t for t in tools}
self.max_iter = max_iter
def run(self, task):
context = f"任务:{task}\n"
for i in range(self.max_iter):
# 思考
thought = self.llm.generate(
context + "\n请思考下一步,若完成请输出[FINAL]结果"
)
if "[FINAL]" in thought:
return thought.split("[FINAL]")[-1].strip()
# 解析行动
action = self._parse_action(thought)
if not action:
context += f"\n思考:{thought}\n解析失败,请重新思考"
continue
# 执行并观察
tool_name, params = action
if tool_name not in self.tools:
obs = f"错误:无工具'{tool_name}'"
else:
try:
obs = self.tools[tool_name].execute(**params)
except TypeError as e:
obs = f"参数错误:{str(e)}"
except Exception as e:
obs = f"执行失败:{str(e)}"
context += f"\n思考:{thought}\n行动:{tool_name}({params})\n观察:{obs}"
return "达到最大迭代次数"
def _parse_action(self, text):
# 从思考文本中提取工具调用
# 实际可用正则或让模型输出结构化格式
import re
match = re.search(r'使用(\w+)\(([^)]+)\)', text)
if match:
name = match.group(1)
params = eval(f"dict({match.group(2)})") # 简化处理
return name, params
return NoneReAct 的优势是容错性强:参数错了可以重试,工具挂了可以换别的,复杂任务能拆多步。代价是延迟高、token 消耗大,一轮任务可能调十几次模型。
参数校验的关键细节
无论哪种模式,参数校验都是必做的。LLM 生成的参数常见毛病:
- 类型漂移:把
"42"当数字用,或把列表序列化成字符串 - 字段缺失:漏传必填参数,或传了工具不认识的字段
- 范围越界:日期格式不对,数值超范围
一个实用的做法是给每个工具配 Pydantic 模型:
from pydantic import BaseModel, Field, validator
class SearchParams(BaseModel):
query: str = Field(..., min_length=1, max_length=200)
limit: int = Field(default=10, ge=1, le=100)
@validator('query')
def no_sql_injection(cls, v):
if ';' in v or '--' in v:
raise ValueError('非法字符')
return v
class SearchTool:
name = "search"
description = "搜索信息"
params_schema = SearchParams
def execute(self, **kwargs):
# 自动校验
validated = self.params_schema(**kwargs)
# 实际调用...
return f"搜索'{validated.query}'的结果"Agent 执行前先用 params_schema 校验,不通过就返回错误让模型重试,比等工具报错再处理更干净。
成本控制与延迟优化
ReAct 模式好用,但账单容易失控。几个省钱的实际做法:
模型分级:简单步骤用小模型(如 GPT-3.5),只有复杂推理才上 GPT-4。规划阶段用便宜的,执行阶段按需升级。
缓存思考结果:同类任务的思考模式高度重复。把常见任务的"思考→行动"链缓存下来,下次直接复用,减少重复推理。
限制迭代次数:设硬上限(如 10 轮),同时监控单步 token 消耗,异常时提前终止。
批量工具调用:如果工具支持,把多个独立调用合并成一批。比如要查三个城市的天气,打包一次请求而不是拆三轮。
一个完整的文档处理示例
把上面这些拼起来,看一个实际能跑的文档处理 Agent:
import os
from pathlib import Path
class FileReaderTool:
name = "read_file"
description = "读取文件内容"
def execute(self, path: str, max_chars: int = 5000):
try:
content = Path(path).read_text(encoding='utf-8')
return content[:max_chars] + "..." if len(content) > max_chars else content
except Exception as e:
return f"读取失败:{e}"
class ClassifyTool:
name = "classify"
description = "按内容分类文档"
def __init__(self, llm):
self.llm = llm
def execute(self, content: str):
prompt = f"将以下内容分类为:技术文档/商务合同/会议纪要/其他\n\n{content[:1000]}"
return self.llm.generate(prompt).strip()
class DocumentAgent:
def __init__(self, llm):
self.llm = llm
self.tools = {
"read_file": FileReaderTool(),
"classify": ClassifyTool(llm),
}
self.agent = ReActAgent(llm, list(self.tools.values()))
def process_folder(self, folder: str):
results = []
for file in Path(folder).glob("*.txt"):
# 构造任务描述
task = f"读取文件 {file},分类其内容,返回类别和摘要"
# 运行 Agent
result = self.agent.run(task)
results.append({
"file": file.name,
"result": result
})
return results
# 使用
# agent = DocumentAgent(my_llm)
# print(agent.process_folder("./documents"))这个例子展示了工具间的依赖(分类工具内部也用 LLM),以及如何用 ReAct 把多个工具调用串成工作流。
常见坑与应对
模型幻觉工具:LLM 有时会坚持调用不存在的工具。应对方法是在提示里明确列出可用工具,并加兜底回复:"若无合适工具,请说明无法完成"。
参数过度生成:模型喜欢给工具塞额外参数。用 Pydantic 的 extra='forbid' 严格拒绝未定义字段,逼模型精简。
循环陷阱:ReAct 可能陷入重复调用同一工具的循环。在上下文里记录已尝试的行动,提示模型"不要重复上一步"。
敏感操作确认:涉及删除、转账等操作时,Agent 应暂停并请求人工确认,而不是直接执行。这需要在工具层加权限标记,Agent 层识别后触发中断。
选型建议
| 场景 | 推荐模式 | 理由 |
|---|---|---|
| 单步查询、格式固定 | 直接调用 | 快、省 token |
| 多步骤、需容错 | ReAct | 灵活、可恢复 |
| 高并发、成本敏感 | 直接调用 + 缓存 | 可控成本 |
| 复杂决策、需解释 | ReAct + 思考链 | 可审计、可调试 |
工具参数传递没有银弹。理解两种模式的 trade-off,根据任务复杂度、延迟要求和预算选合适的方案,比盲目追新更实在。