跳到正文

目录

CocoIndex:为 AI Agent 打造的增量索引引擎

CocoIndex:为 AI Agent 打造的增量索引引擎

RAG 系统的数据会过时。代码在改、文档在更新、Slack 消息在涌入,而多数索引管道的应对方式是全量重跑——数据量一大,重跑就要以小时计,期间 Agent 拿到的一直是旧上下文。

CocoIndex 改掉的就是"全量重跑"这个默认操作。代码变更时只重新处理变更文件,Python 转换函数升级时只重新执行输出依赖该函数的部分,其余全部走缓存。运维成本从 O(全量) 降到 O(变化量)——这个差值,随着语料规模增长只会越拉越大。

官方对它的定位经历了一次升级:早期叫"为 RAG 和实时 AI 应用做增量索引",现在的仓库描述是 Incremental engine for long horizon agents——不再只服务于检索,而是把"持续新鲜的上下文"当作 Agent 长任务运行的基础设施。

GitHub: cocoindex-io/cocoindex Stars: 11,632(截至 2026-09-30) 版本: v1.0.24(2026-09-20 发布) 语言构成: Rust 引擎 + Python 声明式 API(GitHub 语言统计 Rust 约 3.1M、Python 约 2.9M 字节) Python: 要求 3.11+,官方支持至 3.13 许可证: Apache-2.0 官网 / 文档: cocoindex.io / cocoindex.io/docs

本文拆解它的四条机制主线各自负责什么、一次代码 commit 如何流过系统、官方 benchmark 数字该怎么读,最后给出什么场景该用、什么场景不该用的判断依据。文中所有版本号和数据均为 2026-09-30 读数。

学习目标

读完本文,你应当能回答这些问题:CocoIndex 靠什么把重算范围压到最小;Target = F(Source) 和传统脚本式 ETL 差在哪;@coco.fn(memo=True) 的缓存键是怎么构成的;官方给出的三个性能数字分别在测什么、不能推出什么;以及面对自己的业务场景,该不该引入它。

目录

这套系统在解决什么

“只同步变化量"听起来是一句话,落地要靠几套彼此独立的机制配合。本文把它们拆成四条主线——这是分析框架,不是官方分类,但对照源码和文档都能找到对应物:

主线职责机制依据
源变化检测识别哪些源数据变了输入内容的哈希
函数版本感知判断转换函数 F 是否变了函数源码的哈希
端到端血缘把每个输出追溯到源字节官方称 100% lineage 覆盖
向量索引同步把变更落到向量库等目标增量 upsert

前两条回答"哪些要重算”:README 对 @coco.fn(memo=True) 的注释写明缓存键是 hash(input) + hash(code),输入和代码任一变化才会触发重算。血缘是回溯通道,调试时回答"这条搜索结果来自哪段源数据",官方把它列为企业版的核心卖点之一。最后一条是落盘动作,变更通过各连接器增量写入目标存储。

为什么增量索引难做

“只处理变化的部分"在工程上有三个绕不开的难点,CocoIndex 的设计基本就是围绕它们展开的。

依赖追踪。一个 chunk 的 embedding 依赖源文件内容,也依赖切分函数、embedding 函数。只看文件哈希,函数改了检测不到;只看函数哈希,文件没改也会白白重算。把两者的哈希组合成缓存键,才能两个方向都不漏。

函数版本感知。转换函数从 v1 改到 v2,理论上所有被它处理过的数据都该重算。但如果只有一部分输出依赖这个函数,全量重算就是浪费。按代码哈希做失效控制,受影响的子集重跑,其余保持不动。

血缘完整性。增量更新最容易出的事故是"索引里有了新数据,但没人说得出它从哪来”。CocoIndex 给每个输出维护一条到源字节的追溯链,这在合规审计场景是硬需求,在调试场景省的是"这条结果为什么是这样"的排查时间。

Target = F(Source):声明式模型

传统数据管道是脚本式的:写一次性脚本处理数据,每次运行都全量处理。源数据变了要手动触发重跑,转换函数改了再重跑一次。数据量一大,这个循环就撑不住——重跑几小时,期间索引一直是旧的。

CocoIndex 把开发者要写的东西换成了目标状态的声明:Target = F(Source)。源或者 F 变化时,引擎负责让目标重新向声明靠拢,且只重算受影响的部分。官方在 README 里给这个心智模型的称呼是 “React — for data engineering”:你声明"要什么",引擎维护"如何保持",和 React 组件里状态驱动视图更新的思路同构。事实上它的 API 也确实带了 React 的痕迹——coco.use_state() 可以给组件声明跨次运行持久化的状态,coco.mount_each() 按数据项挂载处理组件,都会让写过前端的人觉得眼熟。

对这个模型的期望要放准。它省掉的是"怎么增量"的工程量,前提是你能把转换逻辑表达成纯函数——函数读到外部可变状态,哈希失效机制就绕开了。这一点在后面的排查一节还会出现。

一次代码 commit 如何流过系统

用一个具体任务把机制串起来。开发者修改了 src/auth.py 里的一个函数,commit 之后,接入了语义代码搜索的 Claude Code 要能搜到新代码。

1. git commit 修改 src/auth.py
2. 源变化检测发现 src/auth.py 的内容哈希变化
   → 只标记该文件为 dirty
3. AST 感知分块只对 src/auth.py 重新切分
   → 其他文件的 chunk 不动
4. embedding 函数没改 → 只对变更 chunk 重新 embedding
   embedding 函数改了 → 所有 chunk 重算
5. 新 embedding 增量写入向量库
   → 未变更 chunk 直接命中缓存
6. Claude Code 发起语义搜索
7. 返回相关 chunk + 血缘信息(来自 src/auth.py 的哪次变更)

第 2 步和第 4 步分别对应源变化检测与函数版本感知,第 5 步的缓存命中情况直接反映增量处理的收益,第 7 步的血缘来自端到端追溯。一次 commit 让四条主线各跑各的,没有任何一步碰"全量"。

CocoIndex-code:为编码 Agent 做的语义代码搜索

CocoIndex 主仓库之外,官方还维护着一个独立仓库 cocoindex-io/cocoindex-code,基于同一套 Rust 引擎做了个开箱即用的代码搜索工具,当前定位是 “AST-based semantic code search that just works”。

它的形态值得注意:本质是一个 CLI 工具 ccc(ccc init 初始化、ccc index 建索引、ccc search 语义搜索、ccc grep 结构化搜索),嵌入本地存储、不需要架数据库。和编码 Agent 的对接方式按推荐顺序是:Skill(npx skills add cocoindex-io/cocoindex-code,装完 Agent 自己决定何时语义搜索)、Claude Code 插件市场、以及 ccc mcp——把它作为 MCP Server 挂给 Claude Code、Codex、OpenCode。也就是说 MCP 只是接入方式之一,不再是产品的全部形态。

切分是 AST 感知的,按语法单元而不是行数硬切,保证每个 chunk 是完整的函数或类。语言支持靠 tree-sitter,官方表格列了 20 多种:Python、JavaScript/TypeScript、Rust、Go 之外,Java、C/C++、C#、Kotlin、PHP、SQL、Shell 都在列。embedding 两种装法:cocoindex-code[full] 内置 sentence-transformers 本地模型(默认 snowflake-arctic-embed-xs,不需要 API key),精简版走 LiteLLM 接云端模型。

benchmark 数字怎么读

官方宣传中反复出现三个数字:70% fewer tokens per turn、80-90% cache hits on re-index、sub-second freshness。数字本身没有配套的方法论文档,所以读的时候要自己补上"在测什么"这一层:

  • 70% 更少 tokens,说的是检索体积——按语义取回必要的 chunk,而不是整个文件塞进上下文。它不等于"回答质量更好",检索精度和回答质量之间还隔着模型能力。
  • 80-90% 缓存命中,说的是重索引时多少 chunk 直接复用。它反映增量机制在代码库这种"每次只动一小部分"的数据形态上的适配度,但没有给出对比基线,不能换算成"比某框架快 N 倍"。
  • 亚秒级新鲜度,说的是从变更到可查询的延迟。规模未指定,代码库越大越难保住这个数,引用时要带上下文。

三个数字分别对应检索精度、缓存效率、同步延迟。单独引用任何一个都容易高估或低估实际收益,合在一起才构成"增量索引在工程上省不省"的判断依据。

快速上手

安装(quickstart 还会用到 Docling 做 PDF 转换):

pip install -U cocoindex docling

数据库配置只需一行环境变量,指向本地文件——默认走嵌入式本地库,不强制架 Postgres:

echo "COCOINDEX_DB=./cocoindex.db" > .env

官方 quickstart(cocoindex.io/docs/getting_started/quickstart)用一个 PDF 转 Markdown 的应用演示完整流程。main.py 全部代码如下:

import pathlib

import cocoindex as coco
from cocoindex.connectors import localfs
from cocoindex.resources.file import PatternFilePathMatcher
from docling.datamodel.accelerator_options import AcceleratorDevice, AcceleratorOptions
from docling.datamodel.base_models import InputFormat
from docling.datamodel.pipeline_options import PdfPipelineOptions
from docling.document_converter import DocumentConverter, PdfFormatOption

_pipeline_options = PdfPipelineOptions(
    accelerator_options=AcceleratorOptions(device=AcceleratorDevice.CPU)
)
_converter = DocumentConverter(
    format_options={
        InputFormat.PDF: PdfFormatOption(pipeline_options=_pipeline_options)
    }
)

@coco.fn(memo=True)
def process_file(
    file: localfs.File,
    outdir: pathlib.Path,
) -> None:
    markdown = _converter.convert(
        file.file_path.resolve()
    ).document.export_to_markdown()
    outname = file.file_path.path.stem + ".md"
    localfs.declare_file(outdir / outname, markdown, create_parent_dirs=True)

@coco.fn
async def app_main(sourcedir: pathlib.Path, outdir: pathlib.Path) -> None:
    files = localfs.walk_dir(
        sourcedir,
        recursive=True,
        path_matcher=PatternFilePathMatcher(included_patterns=["**/*.pdf"]),
    )
    await coco.mount_each(process_file, files.items(), outdir)

app = coco.App(
    "PdfToMarkdown",
    app_main,
    sourcedir=pathlib.Path("./pdf_files"),
    outdir=pathlib.Path("./out"),
)

运行走 CLI:

cocoindex update main.py

第一次运行做全量回填。之后加一个 PDF、改一个 PDF、删一个 PDF,再跑同一条命令——只有受影响的文件被重新处理,删掉的源文件对应的目标文件也会被自动清理(localfs.declare_file() 声明的是目标状态,不是一次性写入)。

README 上还有一段更贴近向量检索的示例:

import cocoindex as coco
from cocoindex.connectors import localfs, postgres
from cocoindex.ops.text import RecursiveSplitter

@coco.fn(memo=True)  # 按 hash(input)+hash(code) 缓存
async def index_file(file, table):
    for chunk in RecursiveSplitter().split(await file.read_text()):
        table.declare_row(text=chunk.text, embedding=embed(chunk.text))

@coco.fn
async def main(src):
    table = await postgres.mount_table_target(PG, table_name="docs")
    table.declare_vector_index(column="embedding")
    await coco.mount_each(index_file, localfs.walk_dir(src).items(), table)

coco.App(coco.AppConfig(name="docs"), main, src="./docs").update_blocking()

这段是 README 的概念示意,照抄跑不通,两处缺口:embed 没有定义(需要自己接入 embedding,库内置了 sentence-transformers 和 LiteLLM 两种现成选项);RecursiveSplitter().split() 在当前版本要求显式传 chunk_size(按字节计,可选 min_chunk_size、chunk_overlap)。把它当作"声明式写法长什么样"的参考,动手时以官方 quickstart 为准。

连接器与目标

经过一年迭代,连接器已经覆盖了主流数据源和目标存储,全部内置在 cocoindex.connectors 包里。

源(Sources):本地文件系统(localfs)、PostgreSQL、SQLite、Amazon S3、Azure Blob、Google Drive、OCI 对象存储,以及 Kafka / Iggy 消息队列。

目标(Targets):PostgreSQL + pgvector、LanceDB、Qdrant、Turbopuffer、Zvec 等向量存储;Neo4j、FalkorDB、SurrealDB 图数据库;BigQuery、Doris、Snowflake 数仓;Kafka、Valkey;以及 localfs 文件输出。README 把它们归为六大类:关系库、数仓、向量库、图库、消息队列、特征库。

转换(Transformations):内置 RecursiveSplitter(递归文本分块,传 language 参数时走 tree-sitter 语法感知切分)、代码结构化匹配(match_code)、实体解析、基于 sentence-transformers 或 LiteLLM 的 embedding,以及任意自定义 Python 函数。

pgvector 路径的索引参数在源码里写得很具体:declare_vector_index 支持 ivfflat 和 hnsw 两种索引方法(默认 ivfflat),cosine、l2、ip 三种距离度量(默认 cosine),另有 lists、m、ef_construction 等调参入口。两个 @coco.fn 写法都合法——coco.App 的第一个参数既接受 AppConfig,也直接接受应用名字符串。

使用场景

代码库 RAG。Claude Code 或 Cursor 接入 CocoIndex-code 后,Agent 每次拿到的都是最新代码库状态,且只传必要的 chunk。最适合大型 monorepo:变更频繁,但每次只动一小部分,全量重索引的成本和延迟都不可接受,增量方案的收益恰好最大化。

持续变化的文档源。Confluence、Notion、Google Drive 这类文档库接入后,更新自动被捕获并增量同步,不需要任何人记得"重建索引"。收益随文档量和更新频率放大。

消息流知识库。Slack 等消息源持续涌入,传统全量索引的成本随历史总量线性增长,增量索引只随新消息量增长。这个差距在语料积累几个月后就会从"无所谓"变成"跑不动"。

需要血缘的合规场景。每个输出都能追溯到源字节——哪份文档、哪个版本。金融、医疗这类审计场景里,“检索结果的出处可解释"不是加分项,是准入条件。

开发者工具

主仓库自带 CocoIndex Skill(skills/cocoindex/),把概念、API 和常见写法整理成一个文件,装进 Claude Code 等 Agent 后,Agent 写出的 v1 代码就能对上真实的 API 形态。官方文档有专门的 Use with AI coding agents 页面讲安装步骤。考虑到本文提到的那两处 README 示例缺口,让 Agent 配合 skill 写代码,比自己对着文档拼更稳。

examples/ 目录下有 20 多个官方示例:代码索引、PDF 向量化、HN 热点话题、会议记录转知识图谱、多仓库摘要、结构化抽取、CSV 转 Kafka——每个都按增量模式写好,可以直接克隆改造。

局限性与适用边界

  1. Python 是唯一的应用层语言。核心引擎是 Rust,但声明式 API、连接器、转换函数都只在 Python 侧暴露。团队没有 Python 工程能力,集成成本会明显高于预期。
  2. 声明式模型有学习成本。习惯了脚本式 ETL 的开发者需要换个思路:不再写"处理过程”,而是声明"目标状态"。跨过去之后维护成本反而低,但跨的过程需要时间,尤其是缓存失效和血缘这些暗线。
  3. 向量后端选择多,但成熟度不均。pgvector、LanceDB、Qdrant 等都有内置连接器,不再是"只能 Postgres"。不过各自的功能覆盖和调优入口不同,选型前值得先在小数据量上验证目标后端的检索质量。
  4. 仍在快速迭代期。从 5 月底的 v1.0.7 到 9 月 20 日的 v1.0.24,四个月走了 17 个版本号,仅 9 月就发了 4 版。API 大方向稳定(README 示例、quickstart 的核心写法保持一致),但细节一直在动——README 首页示例连 RecursiveSplitter.split 必填的 chunk_size 都没传,照抄会报错。生产使用建议锁版本,并订阅 release notes。

采用顺序建议

判断标准其实只有两个:数据会不会持续变,以及变的部分占比有多小。

数据持续变化且每次只动一小部分的团队,收益最直接——大型代码库加编码 Agent 是最典型的组合,文档持续更新的知识库其次。这两类现在就可以试,从 quickstart 到生产路径官方材料是齐的。

数据源基本不变的小规模 RAG,先别引入。全量索引的成本本来就低,声明式模型的适配成本却省不掉,等变更频率真成为瓶颈再迁移不迟,迁移成本也不高——毕竟声明式写法的代码量不大。有强合规血缘需求的场景是例外,即使规模不大,血缘一项就值得单独评估。

常见问题与排查

Q: 重跑后所有 chunk 都被重新 embedding,缓存命中很低。

检查转换函数是否带了 @coco.fn(memo=True),没带就不会缓存。再检查函数是否引用了外部可变状态——环境变量、系统时间、全局配置,任何一个都会让输入哈希不稳定,每次都判为"变了"。

Q: 函数没改,但相关输出全部重算了。

大概率是函数隐式捕获了外部可变对象(全局列表、配置字典)。CocoIndex 的代码哈希基于函数源码,捕获的外部依赖不在哈希覆盖范围内时,行为就不可预期。把外部依赖改成显式参数传入,让它们进入输入哈希。

Q: 查询结果包含旧数据。

确认同步是否完成。用 update_blocking() 时它会等更新结束再返回,若走异步的 update() 或 CLI,没等跑完就查询会拿到旧状态。可以查血缘信息确认数据对应的源版本——血缘显示旧版本,说明同步还没追上。

Q: 大文件切分后 chunk 数量过多,索引慢。

调 RecursiveSplitter.split 的参数:chunk_size(按字节,必填)、min_chunk_size(默认一半)、chunk_overlap。chunk 太小,embedding 次数多、API 成本高;太大,单个 chunk 混进太多语义,检索精度下降。代码类内容给 language 参数走语法感知切分,文档类内容从 500-1000 字节配 50-100 字节重叠试起,再按检索效果调。

自测题

  1. CocoIndex 靠哪两套哈希把重算范围压到最小?各自覆盖什么变化?
  2. Target = F(Source) 和脚本式 ETL 的差异是什么?声明式模型把哪些工程量转移给了引擎?
  3. @coco.fn(memo=True) 的缓存键怎么构成?什么情况下会失效误判?
  4. 官方三个 benchmark 数字(70% tokens、80-90% 缓存命中、亚秒级新鲜度)分别在测什么?哪些结论不能从它们推出?
  5. 官方 quickstart 现在用什么数据库、什么命令运行?localfs.declare_file() 和一次性写文件的区别在哪?

练习

  1. 按"快速上手"跑通官方 PDF 转 Markdown 应用,然后依次增、改、删一个 PDF,观察每次 cocoindex update 的处理范围。
  2. 在向量检索示例的基础上补全 embed(用内置的 sentence-transformers 选项),给 RecursiveSplitter().split() 加上 chunk_size,对一个本地文档目录建立索引,验证第二次运行只处理变更文件。
  3. 写一个带 @coco.fn(memo=True) 的转换函数,先在函数里读一次系统时间,观察缓存是否失效;再把时间改成参数传入,对比前后行为。
  4. 安装 CocoIndex-code(pipx install 'cocoindex-code[full]'),在 Claude Code 里通过 Skill 或 ccc mcp 接入,对比语义搜索和 grep 在"找一段逻辑"时的差异。
  5. 准备一个千级文档目录,分别用全量脚本和 CocoIndex 各跑一轮更新(改 5% 的文件),对比耗时与 embedding 调用次数。

进阶路径

  1. 跑通官方示例:从 quickstart 开始,再读 Core Concepts,把状态驱动、组件挂载、memoization 三个概念对上代码。
  2. 接入真实数据源:把团队 Wiki 导出或一个 S3 桶接入,对比全量与增量的耗时、API 调用次数,记录缓存命中率随更新频率的变化。
  3. 试用 CocoIndex-code:在自己的仓库上跑 ccc init + ccc search,观察上下文 token 消耗变化,再决定要不要常驻接入。
  4. 写自定义转换:用 @coco.fn(memo=True) 实现实体抽取或摘要生成,故意改一次函数体,验证只有依赖它的输出被重算。
  5. 生产化部署:锁定版本、评估向量后端选型、监控缓存命中率,并设计函数升级的回滚路径——保留上一版函数哈希对应的历史数据,出问题时可快速切回。

项目信息

参与讨论

使用 GitHub 登录。欢迎补充事实、异议与实践。