需要先将文本切成片,然后才能做向量化。
一、切分方案
不同的类型使用的切分方案不一样,像文本,我们以前尽量按照标题、结构切分,代码按照AST解析切分。但说实话,随着模型能力、上下文增加,工程手段带来的收益可能也没那么大了。
文本切分方案对比
| 方案 | 原理 | 优点 | 缺点 | 适用场景 | 工具 |
|---|---|---|---|---|---|
| 固定长度切分 | 按字符/token数硬切 | 简单、快、好实现 | 容易切断语义 | 快速demo、简单文本 | CharacterTextSplitter |
| 递归字符切分 | 按优先级尝试不同分隔符(段落→句子→词) | 比固定长度智能,尽量保持语义 | 还是比较粗暴 | 大多数通用场景(默认首选) | RecursiveCharacterTextSplitter |
| 按句子切分 | 按句号/问号/感叹号切 | 每个chunk是完整句子 | 句子长短不一 | 对语义完整性要求高 | spaCy、NLTK、jieba |
| 按标题/结构切分 | 按Markdown/HTML标题层级切 | 利用文档结构,语义边界清晰 | 要求文档有规范结构 | 技术文档、知识库、书籍 | MarkdownHeaderTextSplitter |
| 语义切分 | 用embedding判断语义相似度突变点 | 最智能,真正按语义边界切 | 慢、费钱、调参麻烦 | 追求质量、预算充足 | SemanticChunker |
| 滑动窗口 | chunk之间有重叠 | 避免边界信息丢失 | 有冗余,存储计算量增加 | 几乎所有场景都建议加 | 各种splitter都支持 |
| 父子切分 | 小chunk检索+大chunk回答 | 兼顾召回精度和上下文完整 | 实现复杂,存储翻倍 | 生产级RAG,追求效果 | ParentDocumentRetriever |
代码切分方案对比
| 方案 | 原理 | 优点 | 缺点 | 适用场景 | 工具 |
|---|---|---|---|---|---|
| 按函数/方法切 | 每个函数/方法一个chunk | 完整功能单元,语义完整 | 函数大小差异大 | 大多数代码检索(推荐) | tree-sitter、AST |
| 按类切分 | 每个类一个chunk | 完整逻辑单元 | 大类可能太长 | 中小型类 | tree-sitter |
| 按文件切分 | 整个文件一个chunk | 最简单,上下文最完整 | 大文件超长,粒度粗 | 小文件、配置文件、工具类 | 直接读文件 |
| AST解析切分 | 用语法树递归切(类→方法→逻辑块) | 最智能,严格按语法边界 | 实现复杂,每种语言单独处理 | 生产级代码检索(推荐) | tree-sitter、LSP |
| 行级+上下文 | 按行切+带前后几行 | 每个chunk都有上下文 | 冗余多 | 代码搜索、定位问题 | 自己实现 |
切分大小参考
| 场景 | chunk大小(token) | 重叠比例 |
|---|---|---|
| 问答/FAQ | 100 ~ 300 | 10% |
| 普通文章 | 300 ~ 500 | 10~20% |
| 技术文档 | 500 ~ 1000 | 15~20% |
| 长文档/书籍 | 1000 ~ 2000 | 20% |
| 代码(函数级) | 按函数,一般 100~500 | 带前后几行 |
| 代码(文件级) | 小文件直接用,大文件按函数切 | - |
最佳实践总结
| 维度 | 建议 |
|---|---|
| 文本首选 | 递归字符切分 + 10~20% 重叠 |
| 代码首选 | tree-sitter 按函数/方法切 |
| 追求效果 | 父子切分(小chunk检索,大chunk回答) |
| 有结构文档 | 按标题切,带上标题路径元数据 |
| 一定要做 | 加重叠、带元数据(来源/标题/路径) |
| 别做 | 固定长度硬切、切太小导致语义不完整 |
二、AST介绍
AST(Abstract Syntax Tree,抽象语法树)就是把代码解析成一棵树状结构,每个节点代表代码里的一个语法元素(函数、变量、表达式等)。
AST 是什么?
大白话:把代码从"一串文字"变成"一棵有结构的树",让程序能理解代码的语法结构。
比如这段代码:
python
def add(a, b):
return a + b
它的 AST 结构大概是这样:
sql
FunctionDef (函数定义)
├── name: "add"
├── args: [a, b]
└── body:
└── Return (返回语句)
└── BinOp (二元运算)
├── left: Name("a")
├── op: Add (+)
└── right: Name("b")
具体实例
我们用 Python 自带的 ast 模块来实际看一下:
输入代码
ini
import ast
code = """
def greet(name):
message = "Hello, " + name
print(message)
return message
"""
解析成 AST
ini
tree = ast.parse(code)
print(ast.dump(tree, indent=2))
输出的 AST 结构
less
Module(
body=[
FunctionDef(
name='greet',
args=arguments(
args=[
arg(arg='name')
]
),
body=[
Assign(
targets=[Name(id='message')],
value=BinOp(
left=Constant(value='Hello, '),
op=Add(),
right=Name(id='name')
)
),
Expr(
value=Call(
func=Name(id='print'),
args=[Name(id='message')]
)
),
Return(
value=Name(id='message')
)
]
)
]
)
对应关系
| 代码 | AST 节点 |
|---|---|
def greet(name): |
FunctionDef 节点 |
name 参数 |
arg 节点 |
message = "Hello, " + name |
Assign 节点(赋值) |
"Hello, " + name |
BinOp 节点(二元运算) |
print(message) |
Call 节点(函数调用) |
return message |
Return 节点(返回) |
AST 能用来干嘛?
1. 代码切分(之前聊的)
按函数、类、方法来切代码,不会切到半截:
python
# 遍历 AST,找到所有函数
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
print(f"找到函数: {node.name}")
print(f"第 {node.lineno} 行 到 第 {node.end_lineno} 行")
输出:
makefile
找到函数: greet
第 2 行 到 第 5 行
2. 代码分析
- 统计有多少个函数/类
- 找未使用的变量
- 检查代码规范
3. 代码转换/生成
- 自动重构代码
- 代码格式化(Black 就是基于 AST 的)
- 代码翻译(Python → JavaScript)
4. 代码搜索
按结构搜,比文本搜精准:
- 找所有调用了
print的地方 - 找所有继承了
BaseModel的类 - 找所有名字以
test_开头的函数
实战例子:提取所有函数名
python
import ast
code = """
def add(a, b):
return a + b
def subtract(a, b):
return a - b
class Calculator:
def multiply(self, a, b):
return a * b
"""
# 解析成 AST
tree = ast.parse(code)
# 遍历所有节点,找函数
functions = []
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
functions.append({
'name': node.name,
'line': node.lineno,
'args': [arg.arg for arg in node.args.args]
})
# 输出结果
for func in functions:
print(f"函数: {func['name']}")
print(f" 行号: {func['line']}")
print(f" 参数: {func['args']}")
print()
输出:
less
函数: add
行号: 2
参数: ['a', 'b']
函数: subtract
行号: 5
参数: ['a', 'b']
函数: multiply
行号: 9
参数: ['self', 'a', 'b']
不同语言的 AST 工具
| 语言 | AST 工具 |
|---|---|
| Python | 自带 ast 模块 |
| JavaScript | acorn、babel parser |
| Java | javaparser |
| Go | go/ast |
| C/C++ | clang AST |
| 多语言通用 | tree-sitter(推荐,40+ 语言) |
和之前聊的关系
之前说的代码切分 、找相似代码,AST 就是基础:
- 代码切分:用 AST 找到函数/类的边界,精准切分
- 代码搜索:用 AST 按结构搜,比文本搜准
- 向量化:先按 AST 切成函数,再每个函数做 embedding
一句话总结
AST 就是代码的结构化表示,把"一串文字"变成"一棵树",让程序能理解代码的语法结构。
想玩的话,Python 直接 import ast 就能用,几行代码就能解析和遍历。
三、开源向量模型对比
BGE-M3就挺好,做个向量而已,不用单独花钱。
| 模型 | 出品方 | 参数 | 维度 | 最大长度 | 语言 | 核心特点 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|---|---|---|---|
| BGE-M3 | 智源研究院 | 568M | 1024 | 8192 | 100+ | 稠密 + 稀疏 + 多向量三合一 | 中文效果好、功能最全、混合检索强 | 模型稍大、CPU 跑慢 | 中文 RAG、混合检索、长文档 |
| Qwen3-Embedding | 阿里通义 | 0.6B/2B/4B/8B | 多种 | 32768 | 119 | 多尺寸可选、超长上下文 | MTEB 榜单第一、长文本强、多尺寸灵活 | 新模型生态稍弱 | 长文档、多语言、追求性能 |
| GTE | 阿里达摩院 | base/large | 768/1024 | 512 | 中英多语 | 轻量高效、平衡好 | 速度快、效果稳、资源要求低 | 输入短、功能单一 | 短文本、嵌入式、资源受限 |
| E5 | 微软 | small/base/large | 384/768/1024 | 512 | 中英多语 | 经典老牌、生态成熟 | 稳定、文档多、社区活跃 | 输入短、中文一般 | 英文场景、传统检索 |
| Jina Embeddings v2/v3 | Jina AI | 小 / 中 / 大 | 多种 | 8192 | 多语 | 长上下文、多尺寸 | 长文本好、开源可商用 | 中文一般、国内资料少 | 长文档、多语言、海外项目 |
| Nomic Embed | Nomic AI | 137M/274M | 768 | 8192 | 多语 | 轻量、开放训练数据 | 小而快、完全开放 | 效果不是顶尖 | 轻量部署、边缘设备 |
| Snowflake Arctic-Embed | Snowflake | 335M/1.3B | 1024 | 8192 | 50+ | 企业级、BGE 兼容 | Apache 2.0、企业支持、稳定 | 中文一般 | 企业生产、合规要求高 |
| mxbai-embed-large | mixedbread.ai | 335M | 1024 | 512 | 多语 | 小模型高性能 | 体积小效果好、速度快 | 输入短 | 轻量部署、快速检索 |
| BGE 系列 | 智源研究院 | small/base/large | 384/768/1024 | 512 | 中英 | 经典稳定、中文好 | 成熟、文档全、社区大 | 输入短、功能单一 | 中文短文本、传统 RAG |
| text2vec | 中文社区 | base/large | 768 | 512 | 中文 | 纯中文优化 | 中文效果好、轻量 | 只有中文、功能少 | 纯中文短文本场景 |
四、向量存储与更新
向量存储的结构、更新方式,各家情况可能不一样。
OpenClaw
像OpenClaw主要是下面的表。

plain
-- src/memory/storage.ts - Schema 定义
-- 元数据表:追踪索引状态
CREATE TABLE IF NOT EXISTS meta (
key TEXT PRIMARY KEY,
value TEXT
);
-- 文件表:增量同步的基础
CREATE TABLE IF NOT EXISTS files (
path TEXT PRIMARY KEY, -- ★ 相对于项目根目录的路径
hash TEXT NOT NULL, -- ★ 内容 SHA-256,用于检测变更
mtime INTEGER NOT NULL, -- 修改时间戳
size INTEGER NOT NULL -- 文件大小(字节)
);
-- 文本块表:核心数据存储
CREATE TABLE IF NOT EXISTS chunks (
id TEXT PRIMARY KEY, -- ★ 复合 Hash ID
file_path TEXT NOT NULL,
content TEXT NOT NULL, -- 原始文本
embedding TEXT, -- JSON 编码的向量
start_line INTEGER,
end_line INTEGER,
hash TEXT NOT NULL, -- 内容 Hash
FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE
);
-- 嵌入缓存表:节省 API 调用
CREATE TABLE IF NOT EXISTS embedding_cache (
content_hash TEXT NOT NULL,
model TEXT NOT NULL,
embedding TEXT NOT NULL, -- JSON 编码的向量
created_at INTEGER DEFAULT (unixepoch()),
PRIMARY KEY (content_hash, model) -- ★ 内容+模型组合唯一
);
-- 向量虚拟表(sqlite-vec)
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_vec USING vec0(
id TEXT PRIMARY KEY,
embedding FLOAT[1536] -- ★ 维度需与模型匹配
);
-- 全文索引虚拟表(FTS5)
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
id,
content,
content='chunks', -- ★ 内容来源于 chunks 表
content_rowid='rowid'
);
其中Chunk的生成方式如下:
plain
// Chunk ID = SHA-256(source:path:startLine:endLine:contentHash:model)
function generateChunkId(params: {
source: string; // 数据来源(如 "file")
path: string; // 文件路径
startLine: number;
endLine: number;
contentHash: string; // 内容 Hash
model: string; // 嵌入模型名
}): string {
const input = [
params.source,
params.path,
params.startLine,
params.endLine,
params.contentHash,
params.model,
].join(":");
return crypto.createHash("sha256").update(input).digest("hex");
}
这种设计有三个好处:
- 天然去重:相同内容、相同模型生成的 Chunk ID 必然相同
- 模型隔离:切换模型后,ID 不同,不会误用旧向量
- 可追溯:从 ID 可以反推出数据来源
自己
像我们可能存文件id、向量、md5、行号、时间、绑定关系这些。
为什么不需要内容?因为拿着行号直接去用户侧找就行。
更新也不需要那么实时,改动的文件重新向量化,老的文件解除绑定关系。
随着模型强大,工程介入的比例会变低。