数据截至 (上游 commit 460c729002dc)
第 6 章 · 巧妙之处、边界与横向对比
本章讲什么: 前五章拆完了机制。这一章做三件事:挑出可以直接搬进你自己项目的设计;诚实列出这个框架的边界和我在源码里查证到的缺陷;最后和兄弟框架比一比取舍。
6.1 巧妙之处(可借鉴的技术)
一、把「结束」做成一次工具调用
妙在哪: 用同一套机制解决了三个问题——循环终止条件、结构化输出、以及「结束」本身也能被约束(可以规定「没查过天气不准交卷」)。
FinalAnswerTool._run 直接写共享 state 的 answer 字段,掐断主循环条件(python/beeai_framework/agents/requirement/utils/_tool.py:52-59)。它的入参 schema 就是你的输出格式:传 Pydantic 模型进 expected_output,拿回来的就是校验过的对象(_tool.py:36-50)。
二、不允许的工具照样出现在提示里,还附上理由
妙在哪: 大多数框架的做法是「不给的工具就不列」。BeeAI 反过来:列出来,标 Allowed: False,再写一句 Reason(utils/_llm.py:176-184 + prompts.py:71-79)。这等于持续告诉模型「你现在该干别的,而且我告诉你为什么」,比让模型对着缩水的工具表瞎猜有效。
真要藏,另有 hidden=True 这一档。两档语义分开,是这个设计的关键。
三、能力探测 + 结构化输出降级
妙在哪: 「强制模型调某个工具」这件事在 API 层面各家支持度天差地别。BeeAI 用一个 tool_choice_support 集合声明能力,撑不住时把工具表编译成 JSON schema 联合类型,用结构化输出把工具调用伪造出来(backend/chat.py:797-820 + backend/utils.py:125-201)。
上层完全不知情——伪造出来的和原生的都是同一个 MessageToolCallContent。
四、fallback_tool:给漏字段的模型补壳
妙在哪: 重写生成 schema 的 model_validate,模型只吐 {"response": "..."} 时自动补上 {"name": "final_answer", "parameters": {...}}(backend/utils.py:169-188)。
更妙的是它是条件性的:final_answer 只在这一轮允许收尾时才被当作兜底目标(_runner.py:150 的 fallback_tool=request.final_answer if request.can_stop else None)。容错不能损害约束。
这里有一处容易读漏的补丁,值得说清楚:
# python/beeai_framework/backend/chat.py:459-461
fallback_tool = options.get("fallback_tool")
if fallback_tool is None and isinstance(tool_choice, Tool):
fallback_tool = tool_choice
也就是说 fallback_tool 不总是 None。prevent_stop 恰好会把 final_answer 摘出允许集(utils/_llm.py:144-146),只剩一个工具时 tool_choice 就被设成那个 Tool 实例(:156-158)——于是兜底目标变成那个被强制的工具。
| 这一轮的状态 | fallback_tool 实际是谁 |
|---|---|
| 允许收尾 | final_answer |
禁止收尾,且 tool_choice 是某个具体工具 | 那个工具(chat.py:460-461 自动填上) |
禁止收尾,且 tool_choice 还是 "required" | None,格式错误就老实报错 |
核心语义没变:禁止收尾时绝不会兜底到 final_answer。补壳只在「已经被锁死的那一个工具」上生效,约束没被削弱。