Grok 中文文档问答 RAG 组合
主线项目走到这里要收口了:前面几节分别讲了文件、集合、引用与工具,这一节把它们拼成一条能跑的中文文档问答链路。拼的时候会暴露一个很具体的问题——中文切片按字数切,语义断在半句上;检索只靠向量,专有名词又常常召回不到。
这一节给一条最小管线:切片、混合检索、带引用生成。
一、问题不是「有没有 RAG」,而是「中文这一层怎么处理」
把英文 RAG 的做法直接搬到中文,通常会遇到三处水土不服:
- 切片:按固定字符数切,中文一句没说完就被截断,向量表示的是半句语义。
- 检索:纯向量对同义表达友好,但对型号、编号、专有名词不敏感。
- 溯源:答案看起来有出处,但引用指向的片段并不支持这句话。
三处都不是模型能力问题,而是管线设计问题。所以这一节的重点不在模型,在切法与合并排序。
二、最小可运行示例:混合检索管线
下面的脚本不联网、不需要真实密钥。它用固定的三段落模拟切片结果,把「关键词命中」与「模拟向量分」加权合并,取前两条。
"""最小 RAG 管线(mock):关键词 + 模拟向量,加权合并排序。"""
CHUNKS = [
{"id": "c1", "text": "缓存从 messages 开头匹配前缀。"},
{"id": "c2", "text": "200k 阈值会让整请求按高档计费。"},
{"id": "c3", "text": "推理强度分四档,high 是默认值。"},
]
def hybrid(query: str, chunks: list[dict], k: int = 2) -> list[tuple[float, str, str]]:
"""关键词分与模拟向量分加权;真实实现把 vec 换成嵌入相似度。"""
q = set(query)
ranked = []
for c in chunks:
kw = len(q & set(c["text"])) / max(len(q), 1)
vec = 1.0 if any(w in c["text"] for w in ("缓存", "阈值")) else 0.2
ranked.append((round(0.6 * kw + 0.4 * vec, 2), c["id"], c["text"]))
ranked.sort(key=lambda x: x[0], reverse=True)
return ranked[:k]
def offline_check() -> None:
for i, (score, cid, text) in enumerate(hybrid("缓存阈值", CHUNKS), 1):
print(f"{i} {score:.2f} {cid} {text}")
if __name__ == "__main__":
offline_check()
真正上线时,把 CHUNKS 换成切片结果、把 vec 换成嵌入相似度、把 query 换成用户问题;hybrid() 的合并逻辑不用改。关键词分与向量分各占多少权重,要用你的评测集调,不要照抄本节的比例。
三、三个环节的中文处理
| 环节 | 中文场景的坑 | 建议做法 |
|---|---|---|
| 切片 | 按字数切,句子断在半句 | 按标题层级与段落切,超长再按句号 |
| 检索 | 纯向量漏专有名词 | 关键词 + 向量混合,各留一票 |
| 生成 | 答案与引用片段对不上 | 让模型只依据检索片段,带引用输出 |
三件要强调的事:
- 切片要保留结构。中文文档的「第几章、第几节」本身就是语义边界;按标题切,比按 500 字切更不容易断义。切完给每片带上标题路径,检索命中时模型才知道它在哪一章。
- 混合检索不是二选一。关键词负责「型号、编号、专有名词」,向量负责「同义表达、口语化提问」;两者加权合并,比单独用任何一种都稳。
- 生成要能回到片段。让答案带引用(见 8.3),并在提示里明确「只依据给定片段回答,片段里没有就说没有」。这句话比任何后处理都更能减少编造。
四、预期输出与异常分支
运行管线,预期输出:
1 0.70 c1 缓存从 messages 开头匹配前缀。
2 0.70 c2 200k 阈值会让整请求按高档计费。
换成真实数据时,若出现异常,按下面顺序定位:
| 现象 | 优先怀疑 | 第一步动作 |
|---|---|---|
| 答非所问 | 切片断了语义 | 打印命中的片段,看是否半句 |
| 专有名词搜不到 | 只有向量检索 | 加关键词通道 |
| 答案没出处 | 提示未要求引用 | 在提示里要求带来源 |
| 引用不支持结论 | 生成越过了片段 | 限制「只依据片段」并校验 |
最后一行尤其重要:RAG 的失败很少是「检索不到」,更多是「检索到了但模型没用它」。所以引用校验和提示约束要一起上。
五、判据卡
| 判断项 | 该这么做 | 不该这么做 |
|---|---|---|
| 切片 | 按标题与段落切 | 按固定字数硬切 |
| 检索 | 关键词 + 向量混合 | 只依赖一种 |
| 权重 | 用评测集调 | 照抄别人的比例 |
| 生成 | 只依据片段 + 带引用 | 允许自由发挥 |
| 失败处理 | 片段没有就说没有 | 编一个看似合理的答案 |
这张卡的重点是中文这一层要单独设计。五条里有三条在说:切法、召回、溯源,每一环都不能直接套英文方案。
六、来源与核验
- 官方 Collections Search:
https://docs.x.ai/developers/tools/collections-search(核验日期 2026-11-04)。collections_search(xAI SDK)与file_search(OpenAI Responses API)的对应关系、max_num_results/limit参数、agentic 多次检索的行为,均以该页为准。 - 官方 Collections:
https://docs.x.ai/developers/files/collections(核验日期 2026-11-04)。嵌入索引、元数据字段与单文件上限来自该页。 - 官方 Citations:
https://docs.x.ai/developers/tools/citations(核验日期 2026-11-04)。citations与annotations的语义来自该页,引用校验见 8.3。 - 本节为第 8 章的原创综合;切片策略、混合检索权重与提示约束为工程实践,不引用任何第三方的具体指标或代码。中文 RAG 的做法因语料而异,请以自己的评测结果为准。
七、动手试
- 拿你的一份中文文档,分别按「500 字」与「标题 + 段落」切一次,各取 5 个片段,比较哪一组更像完整语义。
- 给
hybrid()加一个kw_weight参数,在 0.3 / 0.5 / 0.7 三档下各跑一遍同一个问题,记录排序变化,并说明你会选哪一档。 - 设计一条「片段里没有答案」的回复模板:既要明确说没有,又不能显得系统坏了。写出你会用的两句话。