项目:会使用工具的助手
做一个库存与报价助手,验证工具选择、错误处理与停止条件。
学完这一课,你将能够
- 组合查询与计算两类工具
- 用固定决策替身验证整个流程
- 交付一组成功和失败用例
项目目标
完成一个库存与报价助手,回答“买 2 本 Python 入门需要多少钱,有货吗?” 系统必须先查询商品,再根据查询结果计算报价。 本项目只读与计算,不创建订单,也不扣款。
建议先完成第三模块前两课。 离线基础版约需 1 小时;接入真实模型后,需要额外测试工具选择质量。
交付标准
- 提供查询和计算两个白名单工具。
- 商品不存在、数量非法和库存不足有明确分支。
- 一次任务最多执行 5 次决策。
- 保存行动与观察,能够解释最终金额来自哪里。
- 至少覆盖成功、未知商品、非法数量和预算耗尽用例。
先把业务规则写清楚
金额使用整数分,避免浮点数计算货币时引入精度问题。 数量必须是正整数;Python 中布尔值是整数的子类,校验时需要特别处理。 商品单价必须来自查询结果,不能由模型凭空提供。 库存不足可以返回报价参考,但本基础版选择直接说明无法满足数量。
完整离线基础版
保存为 tool_assistant.py 并运行。decide 是明确的固定决策替身。
import json
CATALOG = {"python-book": {"stock": 3, "unit_cents": 6800}}
def lookup(args):
if not isinstance(args, dict) or set(args) != {"product_id"}:
return {"ok": False, "error": "invalid_fields"}
key = args["product_id"]
item = CATALOG.get(key) if isinstance(key, str) else None
return {"ok": True, **item} if item else {"ok": False, "error": "not_found"}
def quote(args):
if not isinstance(args, dict) or set(args) != {"product_id", "quantity"}:
return {"ok": False, "error": "invalid_fields"}
quantity = args["quantity"]
if type(quantity) is not int or not 1 <= quantity <= 100:
return {"ok": False, "error": "invalid_quantity"}
item = lookup({"product_id": args["product_id"]})
if not item["ok"]:
return item
if quantity > item["stock"]:
return {"ok": False, "error": "insufficient_stock"}
return {"ok": True, "total_cents": item["unit_cents"] * quantity}
TOOLS = {"lookup": lookup, "quote": quote}
def decide(request, trace):
if not trace:
return {"tool": "lookup", "args": {"product_id": request["product_id"]}}
last = trace[-1]["result"]
if not last["ok"]:
return {"final": f'未完成报价:{last["error"]}'}
if len(trace) == 1:
return {"tool": "quote", "args": request}
cents = last["total_cents"]
return {"final": f"有货,总价 {cents // 100}.{cents % 100:02d} 元。"}
def run(request, max_steps=5):
trace = []
for _ in range(max_steps):
action = decide(request, trace)
if "final" in action:
return {"status": "completed", "answer": action["final"], "trace": trace}
name = action.get("tool")
if not isinstance(name, str) or name not in TOOLS:
return {"status": "failed", "error": "unknown_tool", "trace": trace}
result = TOOLS[name](action.get("args"))
trace.append({"action": action, "result": result})
return {"status": "budget_exhausted", "trace": trace}
for request in [
{"product_id": "python-book", "quantity": 2},
{"product_id": "missing", "quantity": 1},
{"product_id": "python-book", "quantity": -1},
]:
print(json.dumps(run(request), ensure_ascii=False, indent=2))第一条应返回总价 136.00 元;另外两条应说明无法完成报价。
这里的 completed 表示流程正常结束,可能以业务失败说明结束。
产品中可以再增加独立的 task_success 字段,用于区分业务结果。
为什么计算工具再次查询商品
查询之后库存或价格可能变化,工具执行层不能完全信任决策层传回的单价。 本例通过商品 ID 在可信目录里重新读取,避免模型篡改价格。 真实订单服务还需要事务、价格有效期和幂等机制;本项目仅做报价。
连接真实模型的扩展
把 decide 换成模型适配器,工具和执行循环保持独立。
向模型提供工具说明、用户需求和已有观察,要求返回经过校验的行动结构。
先对固定样本重复运行,观察它是否跳过查询、误填数量或在失败后继续计算。
不要把离线基础版标成“真实模型实测”。
验收清单
新增库存不足、数量为 true、未知工具和最大步数为 1 的用例。
将每次运行的状态、答案、工具序列和预期写入一份报告。
合格交付包含代码、运行说明、用例结果和已知局限。 如果只有离线替身,应明确标注;如果接入模型,应记录提供商、模型配置和测试日期。
延伸阅读
- Microsoft:AI Agents for Beginners
- Python:decimal 模块,学习更复杂的十进制金额处理。
接下来让助手不仅能操作工具,还能从你的文档中找到证据。
让这一课,真正成为你的收获
完成练习后标记完成,也可以随时回来复习。
笔记与进度保存在当前浏览器,无需登录