返回事实,而非解释:为什么 LLM 工具应该比你想象的更笨
Source: Dev.to
Part 1: The Problem
1.1 The Helpful Tool That Made Everything Worse
当我第一次为 Verdex 构建 resolve_container 时,我希望它能提供帮助。该工具会从目标元素向上遍历 DOM 树并返回祖先链。我加入了对结果的解释:
{
"type": "product-card", // 工具猜测语义含义
"role": "list-item", // 工具猜测结构用途
"confidence": 0.85, // 工具评估自己的猜测
"recommendation": "Use this as your container scope"
}
起初这看起来还算合理——工具预先分析了结构并给出建议,这样 LLM 就不必再做判断了。
然而在生产环境中,一个包含用户资料卡片的页面被错误标记为 “product‑card”(置信度 0.85)。LLM 信任了工具的解释,生成了针对错误模式的选择器,导致边缘案例的测试失败。
问题不在于解释通常是错误的——它们大多数时候是正确的。根本问题在于:解释是依赖上下文的,而工具缺乏上下文。
1.2 The Core Problem: Interpretation Is Context‑Dependent
考虑一个 <div data-testid="product-card">。它到底意味着什么?
| 任务 | 解释 |
|---|---|
| 选择器编写 | 稳定的容器;使用 getByTestId("product-card") 作为作用域 |
| 可视化测试 | 组件边界;对整个卡片进行截图 |
| 网络爬取 | 数据结构;从子元素中提取商品信息 |
| 可访问性审计 | 语义分组;检查 ARIA 标签 |
同一个 DOM 元素在四种任务中产生了完全不同的含义。当我的工具只选择了一种解释(“这是一个 product card,用它来做选择器作用域”),它把这个决定套用了到所有任务上,尽管它并不知道用户的查询、领域知识以及任务上下文。只有 LLM 才拥有这些信息。
1.3 The Insight: Capability vs. Interpretation
工具提供能力: 访问结构化事实,这些事实否则会被隐藏或获取成本高昂。
LLM 提供解释: 决定这些事实在特定查询、特定上下文中的含义。
工具的职责是遍历 DOM 并返回它找到的内容——标签、属性、深度、关系——而不是 猜测语义类型或规定使用方式。
之前(解释混在一起)
{
"container": {
"semanticType": "product-card", // 工具在猜测
"stability": "high", // 工具在评估
"recommended": true // 工具在规定
}
}
之后(纯粹事实)
{
"ancestors": [
{
"level": 1,
"tagName": "div",
"attributes": { "data-testid": "product-card" },
"childElements": 5
}
]
}
第二种版本对人类来说似乎不那么“贴心”,但对 LLM 更有用,因为它保留了可选性。相同的原始事实可以根据用户的查询进行不同的解释:
- 选择器编写: “第 1 层的
data-testid是一个稳定的容器,我会用它来做作用域。” - 调试: “有 12 个元素使用了该 testid,可能是组件复制时忘记更新 ID。”
- 重构: “这个模式出现在 47 个测试文件中,需要谨慎迁移。”
这种架构之所以有效,是因为能力层保持了解释的独立性。
Part 2: Why This Matters
2.1 The Composition Problem
当工具返回解释时,它们会做出难以逆转的决定。LLM 必须要么接受解释,要么与之抗争——两者都代价高昂。
例子:工具输出 "type": "product-card" 并附带 "confidence": 0.85。用户询问:“在此页面上找出所有用户资料卡片”。LLM 看到工具的解释后有两种选择:
- 信任它(错误): 为 product card 生成选择器。
- 与之抗争(尴尬、消耗 token、可靠性低): 解释为什么工具的解释与查询不匹配。
如果工具返回原始事实("data-testid": "product-card"),LLM 可以检查实际页面结构,发现 testid 具有误导性,并相应地进行调整。
原则: 返回事实的工具可以在不同任务之间组合使用;返回解释的工具只针对单一任务进行优化,容易在其他任务中失效。
2.2 The Human API Trap
面向人的 API 往往刻意高层且带有主观意见。像 page.selectDropdown("Country", "United States") 这样的方式对开发者来说很美好,因为它隐藏了繁琐细节。
然而 LLM 更适合使用低层原语:
page.click('select[name="country"]')
page.click('option:has-text("United States")')
低层操作让 LLM 能够将模式适配到新组件、定制框架或非标准实现上。高层抽象只能在它们被设计的特定场景中工作,会限制 LLM 的灵活性。
这就是为什么 resolve_container 应该返回带有原始属性的祖先链,而不是 “这是你的推荐容器”。这样 LLM 就可以自行决定如何在后续任务中使用这些信息。