AI 资助搜索器不信任自己的模型:FundFinderAI 的防幻觉工程
钱已经存在,小 NGO 就是找不到。USAID 资金在 2025–26 年崩塌后,只有一个资助方的组织现在需要六七个,而做搜索的人就是运营项目的人:一个同时兼任资助书写手的主任,晚上加班。"慷慨"不是稀缺品,注意力才是。所以 AI 资助搜索器是个显然的主意,也是个危险的主意——失败模式不是"没帮助":一个三人 NGO 花一周写一份针对一个根本不存在的截止日期的申请,丢掉的是找不回来的一个星期,而且要到提交时才发现。
FundFinderAI 的回应一句话概括:搜索实时网页找当前开放的、适合你的 NGO 的资助,然后拒绝信任自己的模型对其中任何一个的判断。Gemini 生成的每个申请 URL 在你看到之前都被独立抓取验证,卡片告诉你我们尝试时发生了什么。有趣的部分不是它搜索,而是它做的所有"证明搜索真发生了、结果真存在"的事。
两种幻觉陷阱
陷阱一:模型可以拒绝搜索而不说一个字。前提是实时搜索 grounding。应用配置了 gemini-3.1-pro-preview,传了 tools: [{ googleSearch: {} }],它六十秒内返回六个结构良好、看似合理的资助。看起来成功了。它根本没在搜索。响应里没有 groundingMetadata——候选只有 content、finishReason、index。没有错误、没有警告、没有拒绝。模型接受了一个它不执行的工具,从记忆里回答,形状完全符合你的要求。信号在输出里,不在 API 里:那次运行编造了一个申请 URL。
把同一份 grounding prompt 发给四个模型:gemini-2.5-flash 17 秒、0 chunks、8 次查询、真实 grounding;gemini-flash-latest 11 秒、0 查询、忽略工具;gemini-3.1-pro-preview 38 秒、0 查询、忽略工具;gemini-pro-latest 27 秒、0 查询、忽略工具。四分之三接受 googleSearch 却从不调用。任何一个都会造出一个正常、可信、无 grounding 的应用。
陷阱二(作者自己的):第一版检查问 groundingChunks 是否非空。按这个标准 gemini-2.5-flash 也算未 grounding,作者差点又换一次模型。看 chunks 列:grounding 的模型 chunks 是零。Grounding chunks 把引用附到散文上,而这个 prompt 要求裸 JSON 无散文,没有可附的东西。grounding 真正运行的信号是 webSearchQueries——实际发出的搜索。这个错误还活在生产 UI 里:应用在 chunks 为空时显示红色"Gemini 可能从训练数据回答",等于在正确搜索了八次的那次运行上自证幻觉。修复是报告搜索本身——现在面板显示 Gemini 实际跑的字面查询,对用户比一串不透明的 vertexaisearch 重定向 URL 有意义得多。
把召回调高,看精度崩
Grounding 正确打开后,搜 Kisumu 的 NGO 返回零资助——"未找到高置信度当前开放资助"——搜索面板显示十次真实 Google 搜索。模型看了然后拒绝表态。作者第一反应是门禁让保守主义多余:让模型给候选、让验证来筛选。于是告诉模型返回空列表算失败,无法确认确切回调的资助方也该进结果。奏效了,而且是笔坏交易:结果从零变六个,其中五个链接是死的——不是真资助方上的猜错路径(回退能处理),是死域名。被要求凑数后,模型开始拼装"听起来完全像真实资助方但不存在"的组织。
修好的规则把线画在搜索结果而不是置信度上:每一个都必须是真实出现在你搜索结果里的组织。如果没在搜索结果里看到这个资助方,它就不进列表,不管听起来多合理。三个真实资助方是好答案;六个带两个编造的是坏答案,因为读者分不清哪个是哪个。 URL 同理,这是最容易编造的部分:用出现的地址;没看到就用资助方主页——短地址比猜的 /grants/apply-2026 真实得多。同档案再跑:五个资助、三个验证、一个无定论、一个被抓住。门禁的职责是抓住漏网的,不是给模型猜的许可——规则越好,门禁越没活干。
门禁本身也有 bug
fundersite 回退——拯救"真实资助方但猜错深路径"的机制——差点是死代码。checkLink 先试 HEAD 并在任何已定结论上立即返回,而 404 是已定结论,所以常规情况(规规矩矩的服务器用 HEAD 回 404)返回 broken,永远到不了下面的根探测。回退只在拒绝 HEAD 的服务器上触发。功能在 demo 里工作、README 里描述准确、对写它的场景不可达。修复是一个条件——让 broken 落到下一步而不是直接返回。
为什么结论刻意不对称
verify-link.ts 抓取模型产生的每个 URL。重要的是它拒绝得出什么结论:broken 只在有正面证据说明页面不在——畸形 URL、DNS 失败、服务器自己的 404/410;unverified 留给一切模糊情况——超时、机器人防护的 403、限流、TLS 怪癖。资助方防火墙挡机器人不是资助是假的证据。把真实资助标记成 broken 和编造一个属于同一类伤害、方向相反,所以检查构建成"大声地不确定"而不是"自信地错"。一个真实运行里抓到 au-eu-youthlab.com——一个自信、合理、完全不存在、DNS 都解析不了的域名。它从没到达用户。这正是整个项目要对抗的失败:给一个可能不存在的组织起草一封温暖专业的信,按钮不该对此沉默。
可运行验证
关于"合理输出不是证据"的文章,中心主张——应用是搜索不是记忆——以可运行检查交付:脚本只和 Gemini API 通信,不碰作者服务器和代码路径,打印哪些模型真正履行 googleSearch,配置的模型没搜索就非零退出。如果项目的前提不再成立,这条命令会失败。仓库见原文链接(GitHub: ainazulfiqar99acc/fundfinder-ai)。
来源:The grant money already exists. My AI kept inventing foundations to spend it on - DEV Community