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

AI 智能体 — 是图不是模型

把上限调高,那一单还是跑不完

在 TT Lab 中继续学习

目标

亲手做一个带循环的图,亲手测量递归上限数的到底是什么,并把触及上限和完成了任务分别记成不同的结果。最后做一个诊断,筛出够不到结束条件的输入。

为什么重要

在 LangGraph 中,循环不是什么特殊语法,只是往回走的边。所以做起来容易,让它结束却很难。即使有结束条件,只要混进一个无论如何也达不到该条件的输入,那一条就会永远转下去——本实验的智能体正是如此。扣分的原因有两个(违禁词、长度),而修订节点只修其中一个。 递归上限在这种时候是最后一道防线,但如果不知道它数的是什么,就会误诊。上限数的不是节点执行次数,而是超级步,并排放置的节点不论多少个,都只算一个超级步。本实验中会亲手测量来确认。 而且,触及上限而抛出异常,与图自己结束,是完全不同的两件事。如果都写进同一格“失败”,以后就分不清问题出在哪里。把上限设为两层(状态中的 rounds 和 recursion_limit),并让内层上限留下结果。 评分器不会相信你写下的说明。它会真正导入你的模块,用任意句子检验规则函数,用任意上限运行图,并把得到的结果与评分器另外计算的值对照。

步骤

  1. 在 /root/work/agroute/route.py 中创建 BANNED、MIN_LEN、PASS、make_draft、score_of、revise_text、State、四个节点(compose、review、revise、ship)、after_review 和 build_graph()。在从 review 分出的条件边上给出路径映射,这一步中 revise 之后是 END。
  2. 加上从 revise 回到 review 的边,构成循环。含有多个违禁词的询问,必须转几圈后通过。
  3. 增加 build_unbounded() 和 run_unbounded(question, limit)。用给定的上限运行故意去掉结束条件的版本,触及上限时返回 {"error": "GraphRecursionError", "limit": 한도, "message": 예외 메시지}(占位符依次为上限值与异常信息)。
  4. 增加 MAX_ROUNDS = 5 和 escalate 节点,让 build_graph() 对任何输入都能不抛异常地结束。触及上限时 outcome 为 escalate。
  5. 增加 chain_graph(n)、fan_graph(n)、min_limit(app, state)。min_limit 要找出把该图运行到结束所需的最小 recursion_limit。
  6. 增加 run_guarded(question, limit),用不同的 status 记录已完成的和触及上限的。
  7. 增加 diagnose(question),筛出这个输入是否能满足规则,不能的话原因是什么(too_short 或 banned)。
  8. 在 /root/work/agroute/route_report.json 和 /root/work/agroute/route_report.md 中记录测得的数字和判断。

参考

区分分支名称与路径映射

在 /root/work/agroute/route.py 中创建三个规则函数、四个节点、after_review 和 build_graph()。给 review 的条件边提供路径映射 {"ship": "ship", "revise": "revise"},这一步中 revise 之后是 END。

条件函数接收状态,返回分支名称。如果让它直接返回节点名称,每次改图的形状都得修改判断代码。score_of 如何扣分是后面步骤的材料,所以请严格遵守说明中的规则。

把修订过的再送去评分

加上从 revise 回到 review 的边,构成循环。有三个违禁词的询问,必须转三圈后流向 ship。

循环不是特殊语法,只是往回走的边。圈数取决于 revise 一次只删一个违禁词这一点。统计圈数的 rounds 由 revise 来加——这样以后才有地方设置内层上限。

亲眼看到触及上限

增加 build_unbounded() 和 run_unbounded(question, limit)。用给定的上限运行故意去掉结束条件的版本,触及上限时返回 {"error": "GraphRecursionError", "limit": 한도, "message": 예외 메시지}(占位符依次为上限值与异常信息),正常结束时把 error 设为空字符串。

异常是 langgraph.errors.GraphRecursionError。消息里直接包含上限数字,所以不要编造,把 str(예외)(占位符为异常对象)原样放进去。build_unbounded() 必须是与 build_graph() 不同的图——因为即使后面的步骤给 build_graph() 加上了上限,这一步也必须继续有效。

留下结果的内层上限

增加 MAX_ROUNDS = 5 和 escalate 节点,并让 after_review 在达到上限时返回 escalate 分支。路径映射中也要加上该分支。现在 build_graph() 对任何输入都必须不抛异常地结束。

触及外层上限(recursion_limit)会抛异常,结果就消失了。内层上限会留下结果——这就是这一步的要点。判断只放在 after_review 一处。如果在节点里也判断是否结束,总有一天两处会不一致。

上限数的是超级步

增加 chain_graph(n)(串联)、fan_graph(n)(并排)和 min_limit(app, state)。min_limit 要找出并返回把该图运行到结束所需的最小 recursion_limit。

从 1 开始逐步调高,每次 invoke,返回第一次成功的值即可。两个图的答案有何不同,是这一步的关键——并排放置的节点不论多少个,都在同一个超级步里运行。状态里有一个加了 Reducer 的键就够了。

区分已完成与触及上限

增加 run_guarded(question, limit=25)。图自己结束时返回 {"status": "done", "rounds": 정수, "outcome": "ship"|"escalate", "score": 정수},触及上限时返回 {"status": "limit", "rounds": -1, "outcome": "", "score": -1}(占位符为整数)。

把两件事写进同一格“失败”,以后就无法区分。所做的只是捕获异常并换一个名称,但这一行在生产环境中差别很大。上限给得小的话,正常输入也会得到 limit——评分器会检验这一点。

筛出修了也不会变好的输入

增加 diagnose(question),让它返回 {"fixable": 참거짓, "reason": ""|"too_short"|"banned", "best_score": 정수, "rounds_needed": 정수}(占位符依次为布尔值、整数、整数)。best_score 是把违禁词删到不能再删之后的分数。

不运行图也能知道——把 revise_text 一直应用到不再变化,再测量那时的分数即可。如果该分数达不到 PASS,这个输入无论转多少圈都通不过。诊断比调高上限便宜,原因就在这里。

用测得的数字汇报

在 /root/work/agroute/route_report.json 中写入 default_recursion_limit、chain_min_limit、fan_min_limit、clean、dirty、stuck、stuck_reason,在 /root/work/agroute/route_report.md 中用 ## 고리를 어디에 두었나(韩文,意为“把循环放在了哪里”)、## 재귀 한도는 무엇을 세는가(韩文,意为“递归上限数的是什么”)、## 상한에 닿은 것과 마친 것(韩文,意为“触及上限的与已完成的”)、## 고쳐도 나아지지 않는 입력(韩文,意为“修了也不会变好的输入”)四节来写。

default_recursion_limit 是不提供 config 时的默认值——不要编造,要一边调高上限一边测量来确认。chain_min_limit 和 fan_min_limit 是以节点数为字符串键的对象(例如 {"1": ..., "2": ..., "3": ...})。clean、dirty、stuck 分别原样放入 run_guarded 的答案。