有谷大脑
返回博客

代码库进知识库,为什么不能按字符数硬切

按 1500 字符切一刀,会把 this.db = db 切成 thi 和 s.db。有谷大脑用 tree-sitter 做 AST 感知切分,只在语法边界下刀,每块带上作用域和依赖上下文。

把公司 Git 仓库丢进知识库,问:「用户登录失败的重试逻辑写在哪?」

系统翻了三个文件,全是错的。问题往往不在模型——在模型看到代码之前,代码已经被切碎了,而切的人不认识代码

最常见的做法是按字符数切:每 1500 个字符一刀。对散文还行,对代码是灾难。下面这段 TypeScript,按每 200 字符切三刀:

export class UserService {
  constructor(db: Database) {
    this.db = db
  }
  async getUser(id: string) {
    return this.db.query(id)
  }
}

第一刀可能落在 this.db = db 中间——两个块单独看都不是合法代码,向量也表达不了完整语义。技术文档和合同有各自的切分逻辑;代码需要的是语法边界,不是字符边界

有谷大脑对代码文件的处理,基于开源库 code-chunk 和 tree-sitter 解析器,核心原则:先读懂结构,再下刀

代码 AST 感知 Chunking:Parse → Extract → Scope → Chunk → Context 五道工序。

RAG 里「块」必须同时满足两件事

代码 RAG 和文档 RAG 共用同一套索引逻辑,但「块」的质量标准不同:

  1. 自身语义完整 — 单独拿出来能看懂,向量才有意义
  2. 大小适中 — 太大浪费上下文、稀释语义;太小丢失关键信息

按字符硬切,两条都不满足。更麻烦的是代码还有三个散文没有的特点:

嵌套结构。 一个 getUser 方法「属于」某个类,类又可能属于某个命名空间。单独切出方法体,不告诉检索系统它是 UserService.getUser,代码库里可能有五个同名方法。

跨行依赖。 函数体里用到的 Database 类型,可能在文件开头第 1 行 import。中间隔了两百行,硬切会把 import 和方法体分开。

关键信息在别处。 找「重试逻辑」,代码里可能一个 retry 字样都没有,全是 for (let i = 0; i < 3; i++)await sleep(backoff)——函数名和注释才点题,它们可能被切到上一块。

五道工序:先理解,后切分

工序做什么产出
① Parsetree-sitter 把源码变成 AST每个节点带字节区间 [start, end)
② Extract捞出函数、类、方法、import实体清单
③ Scope按嵌套关系组织作用域树
④ Chunk在语法边界贪心开窗、合并若干代码块
⑤ Context拼签名、依赖、邻居contextualizedText

前三步都在「读懂结构」,第四步才真正下刀,第五步给每块贴身份标签。切分算法自始至终不算字符串拼接,只算区间起止,最后 code.slice(start, end) 一刀切——和原文字节级一致。

为什么选 tree-sitter

容错。 代码可能正在编辑中、括号还没配对。传统编译器遇到语法错误直接罢工;tree-sitter 尽量解析能解析的部分,出错处标成 ERROR 节点继续往下走。

语言无关。 引擎一份,每种语言配一个语法包(WASM)。有谷支持的代码入库覆盖 TypeScript/JavaScript、Python、Rust、Go、Java 等常见语言,代价只是多引几个语法包。

够快。 对一个 17KB 的源文件,解析 + 切分在毫秒级——批量索引代码库时不会成为瓶颈。

contextualizedText:每块带上的「身份证」

检索命中一块代码后,喂给模型的不只有块正文,还有加工过的前缀上下文,通常包括:

  • 文件路径和语言
  • 所属 class / namespace 的签名
  • 直接 import 的依赖
  • 相邻块的函数名(邻居提示)

这样模型看到 return this.db.query(id) 时,知道 this.db 来自哪、getUser 属于哪个服务类——即使块本身没有包含完整的 class 声明。

和通用 chunk 的实际差异

同一份 800 行的 TypeScript 服务文件:

  • 512-token 硬切:约 30–40 个 chunks,大量从方法中间切断;检索「重试」可能命中一个不含函数名的循环体
  • AST 感知切分:按 class、function、interface 边界切,约 15–25 个 chunks;每块是一个可独立理解的语义单元,且带作用域前缀

还有一个常见误区:把代码转成 Markdown 再按段落切。Markdown 渲染会丢掉类型信息、import 关系和语法结构——对代码库,结构比排版重要

什么时候代码 RAG 最容易出问题

问跨文件的调用链。 「AuthService 怎么调 UserRepository?」——需要多块各自完整,且 import 路径可被关联。

问错误处理和重试。 逻辑分散在 try/catch、循环和 util 函数里,硬切容易只命中循环体。

Monorepo 同名符号。 多个 package 里都有 utils.tsgetConfig(),没有文件路径和作用域前缀,检索几乎无法消歧。

正在编辑中的文件。 语法不完整时,容错解析 + 在 chunk metadata 里标记 parseError,比直接跳过或硬切更安全。

和知识库其他格式的关系

有谷大脑对不同格式走不同专用路径,而不是「全部转 PDF 再 OCR」:

  • Excel — 表格语义解析
  • 合同 — 条款级切分
  • PDF — 版面检测 + OCR
  • 代码 — AST 感知切分

共用同一套向量索引和问答界面,但入库时的「第一道工序」必须尊重每种格式的结构。代码库是企业知识库里被低估的一类资产——把代码当成代码来切,而不是当成一段普通文本,是让它在 RAG 里真正可用的前提。