TT Lab
开始
学习 学习路径 课程

AI 智能体 — 是图不是模型

那一次参数是编出来的调用

在 TT Lab 中继续学习

目标

用代码写出调用工具一方的纪律。把契约表放在一处,在调用之前检查选好的参数,不轻信返回的结果而去检查,把失败当作值而不是异常来处理并把原因留在状态里,根据这个原因修正下一次尝试,并且记住,使同样的参数不被调用两次。

为什么重要

接上了工具的智能体,第一次出故障的位置不是模型,而是调用边界。模型编造一个参数名,TypeError 就会蹦到图之外,整个运行就结束了,状态和走过的路径都不会留下。更糟的是不抛异常的情况——工具返回了空列表,却在它上面生成了“3 个地点有库存”这样的句子。 所以本实验从把契约写成值开始。把名称、参数 schema、返回的形态、实际函数绑进一张表,就不必为每个工具各写检查的代码,调用的门也被收窄到一处。在那扇门前检查参数,在门后检查结果,在门前放一个记忆。 选择参数的位置,本实验用规则代替。真正的智能体会在那个位置(pick 节点)去问模型工具名称和参数。其余结构完全一样——反而模型选得越多,调用之前的校验就越必要。 评分器不会相信你写下的说明。它会真正导入你的模块,每次用不同的值检验校验函数,并数出工具主体运行了几次,以确认“是否在调用之前就拦下了”。sku、地区、数量每次运行都会变。

步骤

  1. 在 /root/work/agtool/tools.py 中创建数据(REGIONS、STOCK)以及 MAX_ROWS、CALLS、两个工具(stock_lookup、legacy_stock)和契约表 TOOLS。
  2. 创建 validate_args(name, args),在调用之前检查参数。通过则返回空字符串,否则返回一个原因。
  3. 创建 call_tool(name, args),只让通过校验的调用真正被调用。异常不向外抛,而是以 {"ok", "value", "error"} 返回。
  4. 创建 validate_result(name, value),让 call_tool 也检查结果。原因是 shape、empty、too_big、range。
  5. 创建 CACHE 和 cache_key(name, args),使同一个工具不会用同样的参数被调用两次。只记住成功的。
  6. 创建 State、四个节点(pick、fetch、reply、giveup)、build_naive()、run_naive(order),把失败当作路径而不是异常来处理。
  7. 增加 MAX_ATTEMPTS、REPAIRS、can_repair、repair 节点以及 build_graph()、handle(order),让它根据原因修正后再次尝试。
  8. 在 /root/work/agtool/tool_report.json 和 /root/work/agtool/tool_report.md 中记录确认的内容。

参考

把契约作为值写在一处

在 /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 取反得到的值。## 실패를 어떤 경로로 다뤘나(韩文,意为“用什么路径处理失败”)一节里要用数字写明尝试上限。