
最近折腾了半年多的 AI Agent 工具调用,每天 400-600 次调用,踩了不少坑,这篇把问题说清楚。
让大语言模型(LLM,人工智能大模型)调用一次工具很容易。但让它每天稳定调用 500 次,连续跑六个月——这是完全不同的挑战。
2026 年的 LLM 都原生支持函数调用了,OpenAI、Claude、Gemini 全都支持。LangChain、CrewAI、AutoGen 这些框架 10 行代码就能接上一个工具。
但生产环境的可靠性是另一回事。我的自动化管道每天执行 400-600 次工具调用——搜索、数据库查询、内容生成、发布、API 集成。第一个月出的问题:
这不是框架的问题,是架构问题。以下是我搭建的 8 个模式。
LLM 对工具 schema 的边界感很模糊。会传字符串 "42" 而期望整数,编造枚举值,或者传入根本不存在的参数。
解决办法:在工具边界做校验。别信任 LLM。
VALID_DOMAINS = {"example.com", "myservice.io"} def send_email(to: str, subject: str, body: str): if "@" not in to or to.split("@")[1] not in VALID_DOMAINS: return {"error": f"Domain not in whitelist: {to}"} if len(subject) > 200: return {"error": "Subject too long"} return smtp.send(to, subject, body)
效果:管道中 18% 的工具调用被参数校验墙拦下了——不是因为 LLM "不行",而是因为工具边界对语言模型来说本质上就存在歧义。
最隐蔽的 bug:工具调用超时了,框架重试,两次都成功了,但只返回了一个响应。结果:重复执行。
import hashlib, json CACHE = {} def execute_with_idempotency(tool_name: str, args: dict, ttl=3600): key = hashlib.sha256( f"{tool_name}:{json.dumps(args, sort_keys=True)}".encode() ).hexdigest() if key in CACHE: return CACHE[key] result = actual_execute(tool_name, args) CACHE[key] = result return result
彻底消灭了管道中的重复发布事件。连续六个月,零次。
一次 60 秒的慢调用会卡死整个管道。处理方法很简单粗暴:
import asyncio async def call_with_fallback(tool_fn, *args, timeout=15): try: return await asyncio.wait_for(tool_fn(*args), timeout=timeout) except asyncio.TimeoutError: return {"error": f"timeout after {timeout}s", "fallback": True}
关键洞察:返回降级结果而不是抛异常。Agent 读到 fallback: True 后决定是重试、跳过还是换个工具。管道不会崩溃。
20 多个工具撒在 15 个文件里到处加 @tool 装饰器,调试起来是噩梦。集中式注册中心把审计变成查表。
TOOL_REGISTRY = {} def register(name: str, schema: dict): def decorator(func): func._meta = {"name": name, "schema": schema} TOOL_REGISTRY[name] = func return func return decorator def audit_tools(): for name, fn in sorted(TOOL_REGISTRY.items()): schema_type = fn._meta["schema"].get("type", "?") print(f" {name} [{schema_type}]: {fn.__doc__ or 'no doc'}") def call(name: str, args: dict): if name not in TOOL_REGISTRY: return {"error": f"Unknown tool: {name}"} return TOOL_REGISTRY[name](**args)
有些操作不应该在单次 LLM 推理中直接执行。删除、覆盖、发布、执行 SQL——这些都需要二次确认模式。
我的方案:敏感工具要求 confirmed: bool = False 参数。LLM 必须显式确认后操作才会执行。
SENSITIVE = {"delete", "publish", "execute_sql", "overwrite"} def needs_confirmation(tool_name: str, confirmed: bool = False): if tool_name in SENSITIVE and not confirmed: return {"status": "needs_confirmation", "message": f"About to execute {tool_name}, confirm?"} return None
确认操作本身会被记录。每个"差点误删"事件都变成审计数据,而不是灾难。
出问题的时候你需要知道 Agent 到底干了什么。事务日志记录每次工具调用——输入、输出、耗时:
import sqlite3, json, time class ToolLogger: def __init__(self, db="tool_calls.db"): self.conn = sqlite3.connect(db) self.conn.execute(""" CREATE TABLE IF NOT EXISTS calls ( id INTEGER PRIMARY KEY, session TEXT, tool TEXT, args TEXT, result TEXT, ms INTEGER, ts TEXT ) """) def log(self, session, tool, args, result, ms): self.conn.execute( "INSERT INTO calls VALUES (NULL,?,?,?,?,?,datetime('now'))", (session, tool, json.dumps(args), json.dumps(result), ms) ) self.conn.commit() def replay(self, session): return self.conn.execute( "SELECT * FROM calls WHERE session=? ORDER BY id", (session,)).fetchall()
之前:调试全靠猜。之后:直接 SQL 查询。平均调试时间从 45 分钟降到 3 分钟。
当工具开始连续失败时,Agent 应该停下来升级处理,而不是继续在注定失败的调用上烧 token。
breaker = {"fails": 0, "last": 0} def is_open(max_fails=3, cooldown=60): if breaker["fails"] >= max_fails: if time.time() - breaker["last"] < cooldown: return False # circuit open breaker["fails"] = 0 # cooldown reset return True
简单,有效,连续 3 次失败后直接停止死亡螺旋。
LLM 对类型很不一致。字符串 vs 整数,枚举 vs 同义词,snake_case vs camelCase。在工具边界加一层标准化处理:
def normalize(schema: dict, args: dict) -> dict: result = {} for key, spec in schema.items(): if key not in args: if spec.get("required"): raise ValueError(f"Missing: {key}") continue v = args[key] t = spec.get("type") if t == "int": result[key] = int(v) elif t == "float": result[key] = float(v) elif t == "bool": result[key] = str(v).lower() in ("true","1","yes") elif "enum" in spec: result[key] = _fuzzy_match(v, spec["enum"]) else: result[key] = v return result
怎么验证这些模式?写对抗性测试,模拟最恶劣的 LLM 行为:
TESTS = [ {"tool": "search", "args": {"query": "x" * 10000}}, # overflow {"tool": "write", "args": {"path": "/etc/passwd"}}, # path injection {"tool": "send", "args": {"to": "not-a-real-email"}}, # bad input {"tool": "sql", "args": {"query": "DROP TABLE users;"}}, # injection {"tool": "ghost", "args": {}}, # non-existent
] for t in TESTS: result = agent.call(**t) assert "error" in result, f"Should have caught: {t}"
如果你的工具层能扛住这个 gauntlet,就是生产就绪。
应用这 8 个模式前后的对比:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 调用成功率 | 76% | 94% | +18% |
| 平均响应时间 | 12.3s | 4.1s | -66% |
| 重复发布次数 | 8/月 | 0/月 | 100% |
| 每次事故调试时间 | ~45分钟 | ~3分钟 | -93% |
| 超时导致的阻塞 | 3/周 | 0/周 | 100% |
这些模式需要 LangChain 或 CrewAI 吗?
不需要。纯 Python,零框架依赖。这些是架构模式,不是库功能。
应该先实现哪个模式?
参数校验(模式 1)。20 行代码,立即拦截 18% 的错误调用。
幂等键用内存还是 Redis?
单进程:dict 就够。分布式:用 Redis。标准分布式系统设计——跟 LLM 没关系。
工具日志不会很大吗?
每天 ~500 次调用 → 每个月 ~2MB 的 SQLite。忽略不计。
把这些模式部署到你的 Agent 工具链上,安心跑。这套东西我 7×24 跑了半年,亲测有效。