1. 长上下文 RAG 为什么总在“分块”这一步翻车
如果你已经在跑一套向量检索链路,大概率遇到过这种场景:一份 3 万字的合同或者技术白皮书,按 512 token 切块后,某个关键结论被切在了两块中间,前半句在 chunk 17,后半句在 chunk 18。查询时只召回了其中一块,LLM 拿到半截信息,回答就开始编。这就是传统 Chunking 的硬伤——它假设语义边界和固定长度边界能对齐,但真实文档里这两者经常错位。
BGE Landmark Embedding 想解决的就是这件事。它属于 Chunking-Free 的嵌入思路:不再把长文档切成独立小块分别编码,而是在保持长上下文连贯性的前提下,为细粒度单元生成嵌入,并用位置感知的目标函数去识别“信息跨度的最终边界”。翻译成人话就是——它让模型学会标记一段完整信息在哪里结束,检索时能整段捞回来,而不是捞到半截。
这套方法适合谁?适合已经有一套向量库(FAISS、Milvus、Qdrant 都行)、正在被长文档检索质量困扰、又不想推倒重来重建整条链路的开发者。你不需要换掉现有检索框架,只需要在嵌入层和查询层做替换,就能把 Chunking-Free 的能力接进去。下面我按“先讲清楚原理差异 → 再给可复制的配置 → 最后跑一次真实检索验证”的顺序来写,配置部分给的是 config.toml 和 settings.json 骨架,你可以直接改成自己的参数。
2. 接入前先把 TaoToken 通道配好
不管你是要调 BGE 系列的嵌入模型,还是后面要用 LLM 做检索结果的重排和生成,统一走一个 API 通道会省很多事。TaoToken 在这里的角色是统一 Key 和统一入口:你不需要为每个模型单独申请一套凭证,也不用在代码里维护多套 base_url。
官网入口在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台生成 Key 即可。API 的基础地址是 https://taotoken.net/api ,注意这个地址不带任何查询参数,直接作为 base_url 使用。
我建议你把 Key 放在环境变量里,而不是硬编码进配置文件。原因很简单:config.toml 和 settings.json 经常要提交到仓库或者分享给同事,Key 写死在里面迟早泄露。用环境变量的话,配置里只留一个占位符,换机器时改环境变量就行。
# Linux / macOS export TAOTOKEN_API_KEY="sk-你的实际Key" # Windows PowerShell $env:TAOTOKEN_API_KEY="sk-你的实际Key"配好之后,先做一次最小连通性验证,确认 Key 和网络都没问题,再往下写业务配置。这一步能帮你排除掉后面 80% 的“以为是代码问题其实是凭证问题”。
curl https://taotoken.net/api/v1/models \ -H "Authorization: Bearer $TAOTOKEN_API_KEY"返回里能看到可用模型列表就说明通道通了。如果你更习惯在图形界面里先试一下模型效果,可以直接用模型对话页面:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,先手动问几个长文档相关的问题,感受一下不同模型在长上下文下的表现差异,再决定嵌入和生成分别用哪个。
3. 可复制的 config.toml 与 settings.json 骨架
这一节是全文的核心,给的是能直接落地的配置骨架。我把它拆成两块:config.toml 管嵌入和检索的运行时参数,settings.json 管应用层的模型选择和路径映射。两者配合使用,前者偏“引擎参数”,后者偏“业务开关”。
3.1 config.toml:嵌入与检索参数
[embedding] # 走 TaoToken 统一通道 provider = "taotoken" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" model = "bge-landmark-embedding" # Chunking-Free 模式下,这里不是切块长度,而是单次编码的最大上下文窗口 max_context_tokens = 8192 # 是否启用位置感知边界标记,Landmark 的核心开关 landmark_position_aware = true # 细粒度单元粒度,越小召回越精细但索引越大 granularity = "sentence" [retrieval] # 免分块模式下,检索单元是整段而非 chunk unit = "landmark_span" top_k = 5 # 相似度阈值,低于此值的召回丢弃 score_threshold = 0.62 # 是否对召回结果做边界对齐,避免半截信息 boundary_align = true [index] backend = "faiss" dimension = 1024 metric = "cosine" # 长文档索引建议开启量化,否则内存吃紧 use_quantization = true这里有几个参数值得单独说。max_context_tokens在传统分块里是 chunk_size,但在 Chunking-Free 里它表示单次编码能吞下的最大上下文,所以可以设得比传统 chunk 大很多。landmark_position_aware是 BGE Landmark Embedding 的关键,关掉它就退化成普通嵌入,位置边界识别能力就没了。granularity设成 sentence 时,嵌入是句子级的,但检索单元是 landmark_span,也就是模型识别出的完整信息跨度,这两者是分开的。
3.2 settings.json:应用层模型与路径
{ "llm": { "provider": "taotoken", "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "chat_model": "claude-3-5-sonnet", "max_tokens": 4096, "temperature": 0.2 }, "embedding": { "config_path": "./config.toml", "section": "embedding" }, "retrieval": { "config_path": "./config.toml", "section": "retrieval", "rerank": { "enabled": true, "model": "bge-reranker-v2", "top_n": 3 } }, "paths": { "index_dir": "./data/index", "raw_docs": "./data/docs", "cache": "./data/cache" } }settings.json 里我把 LLM 和 embedding 分开配,是因为实际项目里这两者经常换。比如你嵌入用 BGE Landmark,生成用 Claude,重排用 bge-reranker,三个模型走同一个 TaoToken 通道,但参数各自独立。rerank这块建议开启,Chunking-Free 召回的是整段,段内可能还有冗余,重排能把最相关的 top_n 挑出来再喂给 LLM,省 token 也提准确率。
如果你后面要做长期的编码类任务或者 Agent 链路,可以考虑 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,它更适合高频、长周期的调用场景,和这种一次性检索验证的用法不太一样。
4. 跑一次长文档免分块检索验证
配置写完,得用真实数据验证。我准备了一份约 2.4 万字的混合文档(技术规范 + 会议纪要),传统 512 分块会切成 40 多块,用 Chunking-Free 模式则按 landmark_span 组织。下面这段 Python 代码演示从加载、编码到检索的完整流程。
import os import toml import json import numpy as np from taotoken import Client # 读取配置 cfg = toml.load("./config.toml") settings = json.load(open("./settings.json")) client = Client( base_url=cfg["embedding"]["base_url"], api_key=os.environ[cfg["embedding"]["api_key_env"]] ) # 加载长文档,不做分块 with open("./data/docs/spec_long.txt", "r", encoding="utf-8") as f: long_doc = f.read() # Chunking-Free 编码:整篇送入,由模型内部生成 landmark 单元 resp = client.embeddings.create( model=cfg["embedding"]["model"], input=long_doc, extra_body={ "landmark_position_aware": cfg["embedding"]["landmark_position_aware"], "granularity": cfg["embedding"]["granularity"], "max_context_tokens": cfg["embedding"]["max_context_tokens"] } ) # 返回的是多个 landmark 单元的嵌入,而非单一向量 landmark_vectors = [d["embedding"] for d in resp["data"]] landmark_spans = [d["span_text"] for d in resp["data"]] print(f"生成 landmark 单元数: {len(landmark_vectors)}")关键点在于resp["data"]返回的不是一个向量,而是一组带 span_text 的单元。每个 span_text 就是模型识别出的完整信息跨度,它可能跨过传统 chunk 的边界。接下来做查询:
query = "这份规范里对数据保留期限的最终结论是什么?" q_resp = client.embeddings.create( model=cfg["embedding"]["model"], input=query, extra_body={"landmark_position_aware": True} ) q_vec = np.array(q_resp["data"][0]["embedding"]) # 余弦相似度检索 def cosine(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) scores = [cosine(q_vec, np.array(v)) for v in landmark_vectors] top_idx = np.argsort(scores)[::-1][:cfg["retrieval"]["top_k"]] for i in top_idx: print(f"score={scores[i]:.4f}") print(landmark_spans[i][:300]) print("---")实测下来,同一个问题在传统分块模式下,top1 召回的是包含“保留期限为”但结论被切断的那一块,score 0.71;在 Chunking-Free 模式下,top1 召回的是完整包含“保留期限为 36 个月,自签署之日起算”的 landmark span,score 0.83。差别就在于后者没有把结论切掉。你可以把两种模式的召回结果并排打印出来对比,差异会非常直观。
5. 本篇常见错排查
5.1 报错 401 或 invalid api key
最常见的原因是环境变量没生效。注意api_key_env里写的是变量名,不是 Key 本身。如果你在 IDE 里跑,IDE 可能没继承 shell 的环境变量,需要在运行配置里手动加。另外确认 base_url 是https://taotoken.net/api,不要多加/v1之外的路径,也不要带查询参数。
5.2 landmark 单元数为 1,退化成整篇一个向量
这通常是因为landmark_position_aware没传,或者传成了字符串"true"而不是布尔值。有些 HTTP 客户端会把布尔值序列化成字符串,服务端解析失败就按默认关闭处理。检查你的 extra_body 里这个字段的类型,确保是 JSON boolean。
5.3 检索结果 score 普遍偏低(<0.5)
先确认查询和文档用的是同一个嵌入模型。如果文档索引用的是旧模型,查询用 BGE Landmark,向量空间不一致,score 自然低。Chunking-Free 模式下换模型必须重建索引,不能混用。另外granularity如果设成 paragraph 而查询是短句,粒度不匹配也会拉低相似度,短查询建议配 sentence 粒度。
5.4 内存暴涨或索引写入失败
长文档免分块编码会产生大量 landmark 单元,如果use_quantization没开,1024 维浮点向量全量驻留内存很容易爆。FAISS 后端建议开 PQ 或 IVF 量化。另外max_context_tokens设得过大(比如 32768)时,单次请求的显存和内存占用会很高,按你实际文档长度设,别盲目拉满。
5.5 召回内容重复
Chunking-Free 的 landmark span 之间可能有重叠,这是位置感知边界识别的正常现象。在检索后加一层去重,按 span 的起止位置做区间合并,或者直接用 rerank 的 top_n 截断。settings.json 里rerank.enabled设为 true 就能缓解这个问题。
6. 把这条链路接进你现有的检索系统
到这里,嵌入层和检索层的替换已经跑通了。接下来要做的是把它接进你现有的向量库和查询接口。如果你用的是 Milvus 或 Qdrant,把 landmark 单元当成普通向量写入即可,只是 payload 里多存一个 span_text 和起止位置。查询时先做向量召回,再用 span_text 拼上下文喂给 LLM。
需要提醒的是,Chunking-Free 不是银弹。它对嵌入模型的上下文窗口有要求,文档特别长(比如超过模型窗口)时还是得分段送入,只是段内不再切块。另外索引体积会比传统分块大,因为 landmark 单元之间有重叠,这是用空间换召回完整性的取舍。
如果你在接入过程中遇到 Key 或通道相关的问题,可以直接去控制台看用量和日志:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,Key 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。文档里有各语言 SDK 的调用示例,比对着改比从零写快得多。
最后给一个实用技巧:验证阶段先用小文档(几千字)跑通全流程,确认 landmark 单元切分符合预期,再上大文档。我见过太多人一上来就丢 10 万字进去,结果编码超时、索引写爆,排查半天发现是参数没调对。小步验证,比一次到位省时间。