AI Agent 应用实战(1):从工具调用理解 Agent 原理
Agent 不是一个“更会聊天”的模型,而是模型、工具、状态与控制循环组成的软件系统。最小闭环是观察输入、决定动作、执行工具、读取结果,再决定回答或继续。
一、痛点:先定义 Agent 的可验收边界
业务方说“让 Agent 自动处理”时,必须追问自动到哪一步、可访问哪些数据、谁承担最终决定。一个可验收任务要写清输入、成功产物、禁止动作、最大步数、时间与费用预算。例如“处理客户请求”太宽;“读取一条已脱敏工单,给出分类、引用政策条款并生成不外发的回复草稿”才可测试。工具调用的首要价值,是把自然语言的不确定性包在确定性软件边界内。
设计时把失败也纳入契约:工具无结果时是拒答、转人工还是换查询;信息冲突时谁有优先级;达到预算时保存什么状态。不要以“模型通常会判断”为验收标准。可观测的终止原因、工具参数和证据引用,才让开发者复盘错误。所有写操作默认关闭,先用内存假实现或沙箱跑通轨迹,再逐级开放权限。
二、原理:模型提议,程序裁决
把模型输出视为不可信的计划,而不是已经执行的命令。模型擅长从自然语言选择动作,宿主程序擅长权限检查、类型校验和确定性计算。二者分开,才能重放每一步,也能在模型选错工具时拒绝执行。
下面程序把三种方案放进相同门槛。它演示的不是模型能力排名,而是“先过硬约束、再在合格项中择优”的决策函数。生产中可把 score 拆为任务成功率、安全违规率、p95 延迟和单次成本,并保存原始样本,不能只存平均分。
from dataclasses import dataclass
@dataclass(frozen=True)
class Candidate:
name: str
score: float
candidates = [
Candidate("直接回答", 0.45),
Candidate("单次工具", 0.78),
Candidate("观察后再决策", 0.92),
]
threshold = 0.80
eligible = []
for item in candidates:
accepted = item.score >= threshold
print(f"{item.name}: score={item.score:.2f} accepted={accepted}")
if accepted:
eligible.append(item)
if not eligible:
raise SystemExit("no eligible design")
selected = max(eligible, key=lambda item: item.score)
print(f"selected={selected.name}")
print(f"margin={selected.score - threshold:.2f}")
运行输出:
直接回答: score=0.45 accepted=False
单次工具: score=0.78 accepted=False
观察后再决策: score=0.92 accepted=True
selected=观察后再决策
margin=0.12
阈值必须在实验前确定,否则团队容易看到结果后移动球门。对高风险动作,应使用一票否决:哪怕综合分高,只要出现越权写入或泄露敏感字段就不能发布。对低风险辅助功能,则可接受较低自动化率,用拒答和转人工换取精确率。
三、实现:把状态、工具与终止条件接起来
最小实现需要工具注册表和运行状态。注册表只暴露允许的名字,不让模型导入模块或拼接命令;状态保存目标、预算、历史与状态码。每次动作按固定顺序经过解析、Schema 校验、授权、执行、结果裁剪和日志记录。工具结果同样不可信:网页可能包含提示注入,数据库文本可能过长,因此只回传任务需要的字段并标注来源。
以下脚本独立可运行,用固定动作模拟 calculator、lookup_policy、send_notice。把模拟决策替换为模型响应时,执行器不需要改变;这正是把概率模型与业务副作用解耦的收益。
from dataclasses import dataclass, field
@dataclass
class RunState:
goal: str
budget: int = 4
history: list[str] = field(default_factory=list)
status: str = "running"
def execute(state: RunState, action: str) -> str:
allowed = {"calculator", "lookup_policy", "send_notice"}
if action not in allowed:
raise ValueError(f"blocked action: {action}")
state.budget -= 1
observation = f"{action}:ok"
state.history.append(observation)
if action == "send_notice":
state.status = "completed"
elif state.budget == 0:
state.status = "budget_exhausted"
return observation
state = RunState(goal="完成可审计任务")
for action in ["calculator", "lookup_policy", "send_notice"]:
result = execute(state, action)
print(f"step={len(state.history)} result={result} budget={state.budget}")
if state.status != "running":
break
print(f"status={state.status}")
print(f"history={','.join(state.history)}")
运行输出:
step=1 result=calculator:ok budget=3
step=2 result=lookup_policy:ok budget=2
step=3 result=send_notice:ok budget=1
status=completed
history=calculator:ok,lookup_policy:ok,send_notice:ok
真实服务还应给每次 run 分配不可猜测的 ID,日志记录 action、参数摘要、耗时、结果摘要和错误类别。不要记录访问令牌、完整个人信息或未经脱敏的提示。历史应追加而非覆盖,关键状态落到事务型存储;这样进程退出后能够判断某个副作用是否已经完成。
四、踩坑:把“能跑”误当成“可托管”
第一类坑是授权过宽。工具名称看似安全,参数却可能扩大范围,例如查询接口接受任意用户 ID,或通知接口接受任意收件人。授权必须结合当前用户、资源和动作在服务端判断。第二类坑是把工具报错原样塞回上下文,既浪费窗口,也可能暴露栈、SQL 和密钥;应转换成稳定错误码与经过裁剪的说明。
第三类坑是缺少停止条件。模型可能重复相同动作、在两个工具间振荡,或因为观察为空而不断改写查询。可用规范化后的“工具名 + 参数哈希”检测重复,并同时限制轮数、墙钟时间和费用。第四类坑是把测试数据与线上分布混为一谈:评测必须覆盖空结果、中文别名、超长输入、权限不足、下游超时以及提示注入。
取舍上,不是步骤越智能越好。确定的字段转换、金额计算、规则路由应该写普通函数;只有分类、信息抽取、证据综合等语义任务才值得调用模型。模型调用增加延迟、成本与不确定性,显式工作流增加代码量却换来可预测性。先选择最简单的可用结构,再依据失败轨迹增加 Agent 自主性。
五、验证:用轨迹而不是演示视频验收
为每个案例保存初始状态、每步动作、工具观察、最终产物和终止原因。至少检查五项:任务是否完成;是否只用了允许工具;参数是否满足类型和业务约束;结论能否追溯证据;是否在预算内停止。回归时固定模型版本、提示版本、工具契约版本和随机参数,否则分数变化无法归因。
上线顺序采用离线回放、影子流量、小比例灰度、有限自治。影子阶段让 Agent 读取真实输入但不产生副作用,与人工结果比较;灰度阶段保留即时关闭开关;只有连续满足质量与安全门槛,才扩大范围。一次失败应能定位到规划、参数、工具、上下文或策略层,而不是笼统归咎于“模型幻觉”。
本篇可交付物包括:一份边界说明、一组工具契约、可重放轨迹、失败分类表和发布门。下一篇将用 function calling 让模型输出可校验的工具参数,继续复用本篇的预算、白名单和审计轨迹。
参考来源
👍 觉得有用就点个 赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。
🚀 本文属于 《AI Agent 应用实战》 系列,持续更新,关注不迷路。
📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到 cj2664@qq.com,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。
更多推荐



所有评论(0)