Exa Canon — 搜索引擎 DAG 编排系统
> 一句话版本:Exa 搜索引擎内部系统。把搜索流程从一堆 if/else 函数调用改造成 DAG(有向无环图),自动并行、自动取消、自动追踪、自动缓存。为 AI Agent 写代码的时代设计的搜索架构。
| 项目 | 信息 |
|---|---|
| 来源 | [exa.ai/blog/composing-a-search-engine](https://exa.ai/blog/composing-a-search-engine) |
| 公司 | Exa(AI 搜索 API) |
| 系统 | Canon(内部搜索编排器) |
| 日期 | 2026 年 |
背景
Exa 每天处理数十亿搜索请求,服务数千个不同的 AI Agent。搜索不再是简单的"查索引 → 排序 → 返回":
- 用户要日文结果?→ 加本地化
- 要新鲜新闻还是经典内容?→ 加新鲜度模型
- 查询命中知识图谱还是产品索引?→ 加分类器
- 每个客户有独特的搜索路径需求
简单请求变成了 20+ 节点类型的 DAG,带很多分支。
核心设计:搜索即 DAG
Canon 把搜索管道定义为 DAG:
{
"nodes": {
"dense": {
"type": "retrieve",
"index": "web",
"numResults": 50
},
"fetch": {
"type": "fetch_content",
"source": "dense"
}
},
"root": {
"node": "fetch",
"output": "docs",
"query": "effect-ts fiber runtime evaluation signal",
"numResults": 10
}
}
DAG 提供的能力:
| 能力 | 说明 |
|---|---|
| 自动并行 | 运行时知道依赖关系,无依赖的节点自动并行 |
| 持久执行 | 节点失败可以从该节点重试,不用从头执行 |
| 可内省 | DAG 是数据,可以可视化、验证、分析 |
| 定义与执行分离 | 同一 DAG 可以同步测试、分布式生产、dry-run 预览 |
DAG 不适用的场景:
- 反应式事件循环(没有可调度的工作)
- 共识协议(需要严格顺序)
- 反馈循环(编译器多轮 pass,DAG 不能表达循环)
运行时设计
Pull-based 系统:节点只在下游消费者请求时才运行(惰性求值)。
关键特性:
- 自动并行:无数据依赖的节点并行执行,最小化总延迟
- 取消传播:父节点取消(超时/断连/竞态失败),所有子节点自动取消
- 菱形依赖缓存:两个节点共享上游祖先时,祖先只计算一次
- 零手动埋点:运行时在每个调用边界自动记录时间、输入、输出、决策
示例流程:
查询进入
├ 并行:web index 检索 / news index 检索 / knowledge graph 检索
│ ↓
│ web index 先返回(赢得竞态)
│ → 取消 news index 和 knowledge graph
│
├ fetch content(拉取 web index 结果)
├ rerank(拉取 fetch 结果,运行一次,缓存)
├ 并行:post-processing
└ 返回结果 + 完整 trace
可观测性
之前的问题:
- 搜索管道是一堆 if/else + 函数调用
- "为什么 x 查询没有返回 z URL?"——需要手动翻日志找 needle in haystack
- 换一个 reranker 要审计所有可能调用它的条件分支
Canon 的方案:
- DAG 可序列化,编译成可追踪的图
- 可以精确追踪"哪个子系统丢弃了 URL,为什么"
- 每个节点的 timing/inputs/outputs/decisions 自动记录
为 AI Agent 写代码的时代设计
核心洞察:2026 年几乎所有代码由 Agent 编写。Agent 需要结构引导它们一次性写对。
旧世界的问题:
- 隐式不变量到处都是:"内容已经 fetch 了吗?""有内容可以 rerank 吗?""处理了审核吗?"
- Agent 能从附近代码学习模式,但全局正确性靠隐式规则
Canon 的解法:
- 把检索编排变成类型化执行图
- 隐式直觉变成显式构造
- 正确性的负担由类型系统 + 图 schema + 运行时承担,而不是 Agent 的上下文窗口
- Totality(全函数):图必须处理每个节点的每个结果,缺失分支在类型检查时就被拒绝
DAG 通俗解释
用搜索 "best restaurants near me" 举例:
旧方式(if/else 串行):
results = search_index("best restaurants near me") # 100ms
localized = localize(results, lang="zh") # 50ms
ranked = rerank(localized) # 80ms
cleaned = moderate(ranked) # 30ms
return cleaned[:10] # 总计 260ms,串行
问题:如果用户用英文搜,localize 白跑 50ms。想加"新鲜度过滤"?要在中间插代码。
DAG 方式:
用户查询: "best restaurants near me"
│
┌────▼────┐
│ classify │ ← 什么类型的查询?→ "local_search"
└────┬────┘
│
┌────┼────────────┐ ← 并行执行
▼ ▼ ▼
web maps knowledge_graph
index index search
60ms 40ms 90ms
│ │ │
│ │ ┌─────┘
│ │ │ maps 先返回 → 取消 knowledge_graph
│ └──────┤ web 也返回
└─────┬─────┘
▼
merge → rerank → moderate → return top 10
总计 ≈ 150ms
| 旧方式 | DAG | |
|---|---|---|
| 并行 | 手写,容易忘 | 自动,运行时算 |
| 加新步骤 | 改代码,怕改坏 | 加个节点,不影响其他 |
| 某步失败 | 手动 try/catch | 运行时自动重试/降级 |
| 调试"为什么没返回X" | 翻日志 | 图上有完整路径 |
| 某个索引慢了 | 全卡住 | 其他先返回的先用 |
菱形依赖缓存:两个节点共享同一个上游祖先时,祖先只计算一次,结果缓存共享。
一句话:DAG 就是一张"任务依赖图"。能并行的全并行,没人需要的任务立刻取消。
分析
优势:
- 🔥 实用主义的架构选择——DAG 恰好匹配搜索管道的形状
- 📊 可观测性是核心设计目标——不是事后补的
- 🤖 为 Agent 写代码设计——类型系统 + schema 承担正确性负担
- ⚡ Pull-based 运行时——惰性求值 + 取消传播 + 菱形缓存
局限性:
- Exa 内部系统,不开源
- DAG 不适合所有场景(反馈循环、共识协议)
- 文章偏高层架构,没有性能数据
与 Jay 的关联:
- OpenClaw 的 cron 任务管道可以考虑类似的 DAG 编排
- Lossless Claw 的 DAG 摘要是类似思路——层级压缩 = DAG
- Browser Harness 的自愈理念和 Canon 的"结构引导 Agent 正确性"异曲同工
- Jay 如果做搜索相关产品,这个 DAG 编排模式值得参考
评分
| 维度 | 评分 (1-10) | 说明 |
|---|---|---|
| 架构设计 | 9 | DAG + pull-based + 全函数,教科书级 |
| 实用性 | 8 | 解决真实的十亿级搜索编排问题 |
| 创新性 | 7 | DAG 编排不新,但搜索场景应用深入 |
| 开放性 | 3 | 不开源,只有博客文章 |
| 与 Jay 的关联 | 6 | 架构思路有参考价值 |
| **总分** | **6.6** | 搜索引擎编排的优秀架构案例 |