• 主页
  • 架构
  • 编程语言
  • 数据存储
  • 网络
  • VMware
  • 服务器
  • 组网
  • AI
  • 算法系列
  • 设计模式
  • 读书笔记
  • 思考
  • 工具
  • 其它技术

  • 主页
  • 架构
  • 编程语言
  • 数据存储
  • 网络
  • VMware
  • 服务器
  • 组网
  • AI
  • 算法系列
  • 设计模式
  • 读书笔记
  • 思考
  • 工具
  • 其它技术

Memory向量记忆系统4-文本向量化

2026-07-26

需要先将文本切成片,然后才能做向量化。

一、切分方案

不同的类型使用的切分方案不一样,像文本,我们以前尽量按照标题、结构切分,代码按照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 是什么?

大白话:把代码从”一串文字”变成”一棵有结构的树”,让程序能理解代码的语法结构。

比如这段代码:

1
2
def add(a, b):
return a + b

它的 AST 结构大概是这样:

1
2
3
4
5
6
7
8
9
FunctionDef (函数定义)
├── name: "add"
├── args: [a, b]
└── body:
└── Return (返回语句)
└── BinOp (二元运算)
├── left: Name("a")
├── op: Add (+)
└── right: Name("b")

具体实例

我们用 Python 自带的 ast 模块来实际看一下:

输入代码

1
2
3
4
5
6
7
8
import ast

code = """
def greet(name):
message = "Hello, " + name
print(message)
return message
"""

解析成 AST

1
2
tree = ast.parse(code)
print(ast.dump(tree, indent=2))

输出的 AST 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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. 代码切分(之前聊的)

按函数、类、方法来切代码,不会切到半截:

1
2
3
4
5
# 遍历 AST,找到所有函数
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
print(f"找到函数: {node.name}")
print(f"第 {node.lineno} 行 到 第 {node.end_lineno} 行")

输出:

1
2
找到函数: greet
第 2 行 到 第 5 行

2. 代码分析

  • 统计有多少个函数/类
  • 找未使用的变量
  • 检查代码规范

3. 代码转换/生成

  • 自动重构代码
  • 代码格式化(Black 就是基于 AST 的)
  • 代码翻译(Python → JavaScript)

4. 代码搜索

按结构搜,比文本搜精准:

  • 找所有调用了 print 的地方
  • 找所有继承了 BaseModel 的类
  • 找所有名字以 test_ 开头的函数

实战例子:提取所有函数名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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()

输出:

1
2
3
4
5
6
7
8
9
10
11
函数: 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 就是基础:

  1. 代码切分:用 AST 找到函数/类的边界,精准切分
  2. 代码搜索:用 AST 按结构搜,比文本搜准
  3. 向量化:先按 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主要是下面的表。

image-20260724133711123

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
-- 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的生成方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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");
}

这种设计有三个好处:

  1. 天然去重:相同内容、相同模型生成的 Chunk ID 必然相同
  2. 模型隔离:切换模型后,ID 不同,不会误用旧向量
  3. 可追溯:从 ID 可以反推出数据来源

自己

像我们可能存文件id、向量、md5、行号、时间、绑定关系这些。

为什么不需要内容?因为拿着行号直接去用户侧找就行。

更新也不需要那么实时,改动的文件重新向量化,老的文件解除绑定关系。

随着模型强大,工程介入的比例会变低。

扫一扫,分享到微信

微信分享二维码
Memory向量记忆系统3-数据选取.md
资治通鉴四-九品中正制:一场注定失败的利益交换
© 2026 John Doe
Hexo Theme Yilia by Litten