beeai-framework — 本课题摘录
读了哪几篇: 01-requirement-agent(主线循环)、02-requirements-and-rules(声明式约束怎么折叠)、03-backend-chatmodel(统一模型层)。
其余三篇(运行时底座、工具与记忆、深入与边界)本轮没读。
这一家贡献一个别家都没有的机制:每一轮动态算出"这一轮允许干什么、能不能收工"。
它对本课题回答了什么
决定一:每一轮把约束折叠成一张"通行证"
一条规则说的是"某个工具在这一轮的处境",只有五个开关:
| 开关 | 模型能在清单里看到吗 | 模型能调吗 | 典型用途 |
|---|---|---|---|
| 默认 | 能 | 能 | 正常 |
| 不允许 | 能(而且标注了"为什么现在不给用") | 不能 | 「现在还不能搜,得先查天气」 |
| 隐藏 | 不能 | 不能 | 权限不足的工具,索性不让模型知道 |
| 强制 | 能 | 必须 | 「第 1 步必须先思考」 |
| 禁止这轮收尾 | — | — | 「至少调一次天气才准交卷」 |
(依据:Agent 库 · BeeAI Framework · 需求与规则:声明式约束怎么折叠成一张「本轮通行证」 —— Rule 有 allowed/prevent_stop/forced/hidden 四个布尔开关加一个 reason,每轮把所有 Requirement 跑成 Rule、按工具分组折叠出「允许集/隐藏集/强制工具/能否结束」,再翻译成模型的 tools 与 tool_choice 参数)
"不允许但可见"和"直接藏掉"的区别是这一家的一个小巧思:
不允许但可见,会把工具连同"为什么现在不给用"一起写进系统提示——等于教模型"你现在该干别的"。 相比之下直接藏掉,模型可能反复瞎猜。
这条对我们有直接用处。 前面 deepagents 说"不支持的工具不能只报错,还得从清单里消失", 这一家说的是另一半:有时候恰恰要让模型看见"这个工具存在但现在不能用",并告诉它为什么。 两条不矛盾:能 力不具备就藏掉,时序不合适就可见但禁用。
"禁止这轮收尾"这个开关是别家没有的: 它不控制某个工具,它控制"能不能结束"。
本课题"什么时候停"这一问,前面所有实现的答案都是"满足某条件就停"; 这一家给了反面:"不满足某条件就不准停"。 两者可以同时存在。
决定二:通行证直接翻译成发给模型的参数
每一轮把所有约束跑成规则,按工具分组折叠,得出四样东西——允许集、隐藏集、强制工具、能否结束——再直接变成发给模型的工具清单和"必须调工具吗"的模式。
关键在"直接翻译":约束不是靠提示词说服模型,而是变成 API 参数。 说服是软的,参数是硬的。能用参数表达的就别写进提示词。
常见时序约束已经被参数化
90% 的场景不用自己写规则,内置的条件约束把常见时序参数化了:
| 参数 | 含义 |
|---|---|
| 第 N 步必须调它 | 算的是成功步数 + 1 |
| A 和 B 都调过之后才允许 | 前置依赖 |
| 一旦 C 调过就不再允许 | 互斥 |
| 上一步是 A 时,这一步强制调它 | 紧接 |
| 不到 N 次不准交卷 | 靠"禁止收尾" |
| 超过 N 次就禁用 | 次数上限 |
| 不许和上一步是同一个工具 | 防原地打转 |
| 计数时是否忽略失败的步骤 | 默认忽略失败 |
(依据:Agent 库 · BeeAI Framework · 需求与规则:声明式约束怎么折叠成一张「本轮通行证」 —— ConditionalRequirement 把常见时序约束参数化——force_at_step(算成功步数+1)、only_after、only_before、force_after、min_invocations(靠 prevent_stop)、max_invocations、consecutive_allowed=False、only_success_invocations 默认 True)
"不许和上一步是同一个工具"这一条是防打转的声明式版本。 cline / cowagent / openmanus 都是"事后检测到打转再干预",这一家是事前就不给它这个选项。
"计数时默认忽略失败的步骤"这个默认值是对的: 失败的调用不该算进"你已经调过 N 次了"。
决定四:收工靠一个"假工具",而且纯文本会被兜底转成它
循环的结束条件是模型调用一个叫"最终答案"的工具。
关键在兜底:如果模型只回了纯文本、没调那个工具,框架会把纯文本硬转成一次"最终答案"调用。 (依据:Agent 库 · BeeAI Framework · RequirementAgent 主线:一次 run 的完整生命周期 —— 循环直到模型调用 final_answer 这个假工具为止;若模型只回纯文本没调,框架用 _create_final_answer_tool_call 把纯文本硬转成一次 final_answer 调用兜底;另有死循环检测,命中就重签通行证)
这条解决了"用完成工具当停止条件"这条路的固有缺陷:模型可能忘了调。 cline / openmanus / smolagents 都用"特殊工具"收工,但都没说"模型忘了调怎么办"。 这一家的答案:忘了就替它调。
死循环检测命中时的动作也很特别:不是停,是"重签通行证"——换一张更严的通行证再给它一次机会。
决定二补充:厂商不支持时,用结构化输出逼出一次合法调用
当厂商不支持所需的"必须调工具"模式时,它把"可选工具"编译成一个 JSON 格式的联合类型,用结构化输出逼模型产出一次合法工具调用,再把结果还原成工具调用消息。 (依据:Agent 库 · BeeAI Framework · 统一 LLM 层:把各家模型的能力差异抹平 —— provider 不支持所需 tool_choice 模式时,ChatModel 把可选工具编译成 JSON schema 联合类型、用结构化输出逼模型产出一次合法工具调用,再把 JSON 还原成 tool-call 消息)
这是"怎么认出模型要调工具"的第五种做法,而且是降级路径: 原生工具调用 → 不支持 → 用结构化输出模拟。 比 crewai 的"降级到文本 ReAct"更强——结构化输出仍然是格式受约束的,不用正则去抠。
它没回答什么
- 历史怎么压——这三篇没讲。
- 循环状态怎么存下来续跑——不在这三篇。
- 并发——不在这三篇。
坑与代价
- 约束系统本身是一层需要学的东西。 五个开关、十个参数、优先级语义——它自己的文档都专门指出优先级语义里有一个容易误解的地方。
判断(无锚): 我们的最小原型不需要这一整套,但**"这一轮允许哪些工具"应该从第一天就是一个可计算的东西,而不是一个固定清单。** 如果错,会错在: 如果只有两个工具且没有任何时序约束,那"每轮算一次"是纯粹的空转,直接给全集即可。
- 每一轮都要跑一遍所有约束。 约束多了就是每轮的固定开销。
- "把纯文本兜底转成收工调用"会掩盖一个真问题:模型为什么没调。 兜底让流程不卡住,但也让"模型不听话"这件事变得不可见。 应该配一条日志。