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

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

一、切分方案

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

  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主要是下面的表。

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");
}

这种设计有三个好处:

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

自己

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

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

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

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

相关推荐
Java编程爱好者1 小时前
Java 并发锁体系全解:从 synchronized、Lock 到 AQS 底层原理与源码深度剖析
后端
程序员天天困1 小时前
Java 项目用 Single-flight 一招终结 AI 接口重复调用
redis·后端
farerboy1 小时前
23-Java 构造函数
java·后端
程序员麻辣烫1 小时前
Memory向量记忆系统3-数据选取
后端·aigc
程序员麻辣烫1 小时前
Memory向量记忆系统2-SQLite
后端·aigc
阳光是sunny1 小时前
LangGraph高级教程:Multi Schema多状态管理详解
前端·人工智能·后端
神奇霸王龙2 小时前
Gemini CLI 中转站配置使用教程
人工智能·ai·ai作画·aigc·ai编程·gemini·goolge
阳光是sunny2 小时前
LangGraph实战教程:状态管理与`graph.invoke`入参深度解析
前端·人工智能·后端
神奇小汤圆3 小时前
深入解析 Flink Kafka Connector:原理、配置与最佳实践
前端·后端