那一次参数是编出来的调用
目标
用代码写出调用工具一方的纪律。把契约表放在一处,在调用之前检查选好的参数,不轻信返回的结果而去检查,把失败当作值而不是异常来处理并把原因留在状态里,根据这个原因修正下一次尝试,并且记住,使同样的参数不被调用两次。
为什么重要
接上了工具的智能体,第一次出故障的位置不是模型,而是调用边界。模型编造一个参数名,TypeError 就会蹦到图之外,整个运行就结束了,状态和走过的路径都不会留下。更糟的是不抛异常的情况——工具返回了空列表,却在它上面生成了“3 个地点有库存”这样的句子。
所以本实验从把契约写成值开始。把名称、参数 schema、返回的形态、实际函数绑进一张表,就不必为每个工具各写检查的代码,调用的门也被收窄到一处。在那扇门前检查参数,在门后检查结果,在门前放一个记忆。
选择参数的位置,本实验用规则代替。真正的智能体会在那个位置(pick 节点)去问模型工具名称和参数。其余结构完全一样——反而模型选得越多,调用之前的校验就越必要。
评分器不会相信你写下的说明。它会真正导入你的模块,每次用不同的值检验校验函数,并数出工具主体运行了几次,以确认“是否在调用之前就拦下了”。sku、地区、数量每次运行都会变。
步骤
- 在 /root/work/agtool/tools.py 中创建数据(
REGIONS、STOCK)以及MAX_ROWS、CALLS、两个工具(stock_lookup、legacy_stock)和契约表TOOLS。 - 创建
validate_args(name, args),在调用之前检查参数。通过则返回空字符串,否则返回一个原因。 - 创建
call_tool(name, args),只让通过校验的调用真正被调用。异常不向外抛,而是以{"ok", "value", "error"}返回。 - 创建
validate_result(name, value),让call_tool也检查结果。原因是shape、empty、too_big、range。 - 创建
CACHE和cache_key(name, args),使同一个工具不会用同样的参数被调用两次。只记住成功的。 - 创建
State、四个节点(pick、fetch、reply、giveup)、build_naive()、run_naive(order),把失败当作路径而不是异常来处理。 - 增加
MAX_ATTEMPTS、REPAIRS、can_repair、repair节点以及build_graph()、handle(order),让它根据原因修正后再次尝试。 - 在 /root/work/agtool/tool_report.json 和 /root/work/agtool/tool_report.md 中记录确认的内容。
参考
- 执行契约:评分器会把
/root/work/agtool/tools.py当作 Python 模块导入,直接使用REGIONS、STOCK、MAX_ROWS、CALLS、TOOLS、stock_lookup、legacy_stock、validate_args、validate_result、call_tool、CACHE、cache_key、run_naive、MAX_ATTEMPTS、REPAIRS、can_repair、handle。它不会作为脚本运行,所以可以没有if __name__ == "__main__"。 - 本实验的数据是这样的。
REGIONS = {"seoul": ["gasan", "guro", "mapo"], "busan": ["sasang", "haeundae"], "jeju": ["hallim"]},STOCK = {"A-1001": {"gasan": 4, "guro": 2, "mapo": 7, "sasang": 1}, "A-1002": {"guro": 5, "haeundae": 3}, "B-2001": {"gasan": 9, "guro": 1, "mapo": 9, "hallim": 2}, "B-2002": {"sasang": 6, "haeundae": 6}},MAX_ROWS = 2。 - 两个工具的签名都是
(sku, region, limit),返回的也都是{"sku": 문자열, "rows": [{"warehouse": 문자열, "count": 정수}, ...]}(占位符依次为字符串、字符串、整数)。rows按数量降序,数量相同则按仓库名称升序。stock_lookup最多只返回limit个,legacy_stock会无视limit,把那个地区的仓库全部返回(模仿旧版本)。 CALLS是按名称统计工具主体运行次数的字典。在两个工具的第一行各加 1。评分器用这个值来确认“是否在调用之前就拦下了”。TOOLS一个条目的形态:{"fn": 함수, "args": {인자이름: {"type": "str"|"int", "required": True, "allowed": [...], "min": N, "max": N}}, "returns": {"sku": {"type": "str"}, "rows": {"type": "list", "max_len": MAX_ROWS, "item": {"warehouse": {"type": "str"}, "count": {"type": "int", "min": 0}}}}}(占位符依次为函数与参数名称)。region的allowed是REGIONS的键,limit的min为 1、max为MAX_ROWS。validate_args的原因:unknown_tool·missing:<이름>·extra:<이름>·type:<이름>·allowed:<이름>·range:<이름>(占位符均为参数名称)。通过则为空字符串。评分器只会传入缺陷只有一个的参数。validate_result的原因:shape(形态、类型不对)·empty(rows是空的)·too_big(rows比max_len长)·range(count超出契约的范围)。通过则为空字符串。call_tool返回的error前面要加上来源:参数一侧是args:<사유>,结果一侧是result:<사유>,工具抛出了异常则是raised:<예외이름>(占位符依次为原因、原因、异常名称)。成功则为空字符串。handle(order)的order是{"sku": ..., "region": ..., "limit": ..., "prefer": ...}。有prefer就用那个工具,没有则用stock_lookup。答案是{"ok", "answer", "rows", "errors", "fixes", "attempts", "calls", "tool_runs"},tool_runs是处理这一条期间工具主体运行的次数。REPAIRS是{"args:allowed:region": "region", "args:range:limit": "limit", "result:too_big": "tool"}。地区修正为FALLBACK_REGION("seoul"),数量修正为MAX_ROWS,工具修正为stock_lookup。MAX_ATTEMPTS是 3。- 节点名称和状态键在同一个命名空间。状态里有
tool键,而节点也叫tool的话,编译会因ValueError: 'tool' is already being used as a state key而崩溃。 - 这个 Pod 没有互联网。
pip install无法使用。langgraph 0.2.60 已经装好了(python3 -c "import langgraph")。 - 官方文档:Workflows and agents · Use the graph API · Graph API overview
- 常见错误:调用之后才检查参数(主体已经运行了)、因为
isinstance(True, int)为真而把True当作数量接受、把空结果当成功放过、连失败也记住导致修好后也调用不了、没有尝试上限而不断修正。
把契约作为值写在一处
在 /root/work/agtool/tools.py 中创建 REGIONS、STOCK、MAX_ROWS、CALLS,以及两个工具(stock_lookup、legacy_stock)和契约表 TOOLS。TOOLS 的一个条目带有 fn、args、returns 三项,两个工具在第一行把 CALLS 加 1。
args 是为每个参数名称写明 type、required,必要时再写明 allowed、min、max 的字典。returns 中写明返回的键的类型以及 rows 的 max_len、item。stock_lookup 最多只返回 limit 个,legacy_stock 会无视 limit,全部返回——为什么需要结果校验,后面要用这个工具来看。请直接使用参考一节的数据表。
调用之前检查参数
创建 validate_args(name, args)。读取契约表,检查缺失的参数、契约中没有的参数、类型、白名单、范围,通过则返回空字符串,否则返回一个原因(如 missing:limit)。
原因名称请原样使用参考一节中的写法。在 Python 中 bool 是 int 的子类型,所以 isinstance(True, int) 为真——数量的位置上来了 True,就要用 type: 拦下。如果是未知的工具名,就是 unknown_tool。缺陷有多个时先返回哪个随意,但评分器只会传入缺陷只有一个的参数。
不用错误的参数去调用
创建 call_tool(name, args)。只真正调用通过校验的调用,异常不向外抛。答案始终是 {"ok": 참거짓, "value": 결과 또는 None, "error": 사유}(占位符依次为布尔值、结果或 None、原因),参数一侧的原因前面加 args:,工具抛出的异常前面加 raised:。
实际函数从 TOOLS[name]["fn"] 取出,用 fn(**args) 调用——把调用的门收窄到一处是要点。校验被拦下就到此为止。评分器会在给出错误的参数之后,检查 CALLS 是否没变。调用之后再捕获异常的做法通不过这项检查。
不轻信返回的内容
创建 validate_result(name, value),并让 call_tool 也检查结果。原因是 shape、empty、too_big、range,被拦下时 call_tool 会在 error 前面加上 result: 返回。
读取契约的 returns 来检查——为每个工具手写的话,工具增加时就会漏掉。rows 为空就是 empty(因为不是错误就当作成功放过,就会编造出答案)。比 max_len 长就是 too_big,legacy_stock 正好会造成这种情况。行中的 count 小于契约的 min,就是 range。
同一件事不问两次
创建 CACHE 和 cache_key(name, args),让 call_tool 记住同一个工具、同样参数的结果。即使参数写的顺序不同,也必须得到同样的键,而且只记住成功的。
把 json.dumps(args, sort_keys=True, ensure_ascii=False) 与名称连在一起,就成了不受顺序影响的键。查找记忆要在通过校验之后、调用之前进行。连失败也记住的话,修好后重新调用的路就被堵死了,所以只放入成功的。评分器会用同样的参数调用两次,检查 CALLS 是否只加了一次,以及失败的调用是否两次主体都运行。
把失败当作路径
创建 State、四个节点(pick、fetch、reply、giveup)、build_naive()、run_naive(order)。fetch 即使失败也不抛异常,而是把原因留在 errors 中,失败后走向 giveup。run_naive 返回 {"ok", "answer", "rows", "errors", "calls"}。
pick 从订单中选出工具名称和参数——真正的智能体会在这个位置去问模型。给 errors、calls 加上接续拼接的 Reducer,给 attempts 加上累加的 Reducer。这一步的图不做修正——调用一次,失败就直接放弃。reply 用 rows 的第一行生成答案句子,giveup 把最后一个原因写进答案。
根据原因修正后再次尝试
增加 MAX_ATTEMPTS = 3、FALLBACK_REGION、REPAIRS、can_repair(reason)、repair 节点以及 build_graph()、handle(order)。如果是能修的原因,就修正参数或工具后回到 fetch;修不了或触及上限,就走向 giveup。
repair 读取 errors 的最后一个原因,按 REPAIRS 表修正,并把修了什么留在 fixes 中。正因为把原因留在了状态里,这里才能用上——如果是作为异常抛出,就只有“出了点问题”。空结果再问一次还是空结果,所以属于修不了的一边。handle 的 tool_runs,用调用前后的 CALLS 之和相减来求。
记录测得的内容
在 /root/work/agtool/tool_report.json 中写入 tools、blocked_before_call、bad_result_reasons、memo_second_call_runs、repaired、attempts、gave_up,在 /root/work/agtool/tool_report.md 中用 ## 도구 계약을 어디에 적었나(韩文,意为“把工具契约写在了哪里”)、## 부르기 전에 무엇을 막았나(韩文,意为“调用之前拦下了什么”)、## 돌려준 결과를 어떻게 믿지 않았나(韩文,意为“如何不轻信返回的结果”)、## 실패를 어떤 경로로 다뤘나(韩文,意为“用什么路径处理失败”)四节来写。
数字不要手写,要实际运行你的模块来获得。blocked_before_call 是用错误的参数调用之后工具主体运行的次数,bad_result_reasons 是 validate_result 对四种坏结果给出的原因排序后的结果,memo_second_call_runs 是用同样的参数第二次调用时主体运行的次数。repaired、attempts 是把地区错误的订单交给 handle 得到的,gave_up 是把出现空结果的订单的 ok 取反得到的值。## 실패를 어떤 경로로 다뤘나(韩文,意为“用什么路径处理失败”)一节里要用数字写明尝试上限。