一、引言:为什么要做 RAG?

只用大模型(LLM)回答企业私有问题有两个致命问题:

  1. 知识滞后:大模型训练数据有截止日期,公司最新的员工手册它根本没见过。

  2. 幻觉:让模型"编"一份公司的请假制度,它会一本正经地胡说八道。

RAG(Retrieval-Augmented Generation,检索增强生成) 是目前最主流的解法:

先把企业文档切分、向量化存进向量数据库;用户提问时,先检索最相关的片段,再把这些片段作为"上下文"塞给大模型,让它只基于真实文档作答

本文用一个完整可运行的例子,带你从零实现一个「员工手册智能问答机器人」:喂给它一份 Word 版员工手册,问"员工有哪些权利",它能精准从手册里找答案。

二、整体架构

整个系统是一个标准的 6 步 RAG 流水线:

 Word文档
   ↓ ① 读取 (python-docx)
 纯文本
   ↓ ② 分片 (按长度切 chunk)
 文本片段列表
   ↓ ③ Embedding (Qwen 文本向量模型)
 向量矩阵
   ↓ ④ 入库 (ChromaDB 持久化)
 向量数据库
   ↓ ⑤ 检索 (用户问题向量 → 相似度 TopK)
 相关片段
   ↓ ⑥ 组装 Prompt + 调用大模型 (Qwen-Plus)
 最终答案

三、环境准备

 pip install python-docx chromadb openai
  • python-docx:读取 .docx 员工手册

  • chromadb:轻量级本地向量数据库(无需起服务,开箱即用)

  • openai:兼容 OpenAI 接口调用通义千问(阿里云百炼 / MaaS 私有实例)

向量模型与对话模型均使用阿里云百炼的兼容模式端点(compatible-mode/v1),只需把 base_urlapi_key 换成你自己的即可。

四、代码逐段拆解

1. 读取 Word 文档

 import os
 from docx import Document
 ​
 def load_file(file_path):
     document = Document(file_path)
     all_text = []
     for para in document.paragraphs:
         all_text.append(para.text)
     return "\n".join(all_text)

python-docx 把每一段(paragraph)的文本抽出来,拼成一个大字符串。paragraphs 只覆盖正文段落,如果手册里还有表格内容,需要额外遍历 document.tables,本文示例只处理正文。

2. 文本分片(Chunking)

 def split_text(all_text):
     chunks = []
     for i in range(0, len(all_text), 200):
         chunks.append(all_text[i:i + 200])
     return chunks

RAG 的核心细节之一:不能把整本文档一次性向量化。太长会超出模型 token 限制,且检索粒度太粗。

这里用最朴素的方式:每隔 200 个字符切一刀。生产环境更推荐用 递归字符切分(按段落/句子边界)语义切分,效果会更好。

3. 文本向量化(Embedding)

 from openai import OpenAI
 ​
 def text_embedding(chunks):
     client = OpenAI(
         api_key=os.getenv("DASHSCOPE_API_KEY"),
         base_url="https://ws-6nx5tbd10wyo5nw2.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"
     )
 ​
     if isinstance(chunks, str):
         chunks = [chunks]
 ​
     batch_size = 20
     embeddings = []
     for i in range(0, len(chunks), batch_size):
         batch_chunks = chunks[i:i + batch_size]
         completion = client.embeddings.create(
             model="qwen3.7-text-embedding",
             input=batch_chunks,
             dimensions=256
         )
         batch_embeddings = [item.embedding for item in completion.data]
         embeddings.extend(batch_embeddings)
     return embeddings

要点:

  • Embedding 模型:使用 qwen3.7-text-embedding,把文本变成一串浮点数向量。

  • dimensions=256:指定输出向量维度,维度越低存储和检索越快(精度略有取舍)。

  • 批量调用:每 20 条一批,避免单次请求过大。

  • 该函数既用于"入库时给文档片段向量化",也用于"查询时给用户问题向量化",两端必须用同一个 Embedding 模型,否则向量空间不一致,检索会失效。

4. 存入向量数据库(ChromaDB)

 import chromadb
 ​
 def save_to_vector_db(chunks):
     embeddings = text_embedding(chunks)
     client = chromadb.PersistentClient()        # 数据落盘到本地 ./chroma 目录
     collection = client.get_or_create_collection(
         name="employee_manual_collection",
         embedding_function=None                  # 我们已自己算好向量
     )
 ​
     ids = [f"chunk_{i}" for i in range(len(chunks))]
     collection.add(
         ids=ids,
         embeddings=embeddings,
         documents=chunks,
     )
  • PersistentClient():持久化客户端,重启后数据还在。

  • embedding_function=None:因为我们手写好了 embeddings,告诉 Chroma 不要再自己算。

  • 每条记录由 ids(唯一标识)、embeddings(向量)、documents(原文)三部分组成。

5. 检索(Query → 相似片段)

 def query_by_chroma(user_query_text):
     user_query_embeddings = text_embedding(user_query_text)
 ​
     client = chromadb.PersistentClient()
     collection = client.get_collection(
         name="employee_manual_collection",
         embedding_function=None
     )
 ​
     results = collection.query(
         query_embeddings=user_query_embeddings,
         n_results=5,
         include=["metadatas", "documents", "distances"],
     )
     return results

把用户问题向量化后,在集合里做余弦相似度检索,取最相关的 n_results=5 条。返回的 documents[0] 就是我们要找的"参考片段"。

6. 组装 Prompt + 调用大模型生成答案

 def generate_answer(query_results, user_query_text):
     retrieval_chunks = query_results["documents"][0]
     content = "\n".join([f"--: {chunk}" for chunk in retrieval_chunks])
 ​
     prompt = f"""
     ## 角色设定
     你是一个专业的人力资源专家,
     ## 任务描述
     根据用户的问题和公司的制度内容,回答问题
     ## 输入数据
     用户的问题:{user_query_text}
     公司制度内容:{content}
     ## 约束
     1:严格基于公司的制度内容回答问题,不要胡编乱造
     2:如果公司的制度内容没有相关信息,请明确说明"暂无此知识"
     3:回答简洁明了,条理清晰
     """
     client = OpenAI(
         api_key=os.getenv("DASHSCOPE_API_KEY"),
         base_url="https://ws-6nx5tbd10wyo5nw2.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
     )
     completion = client.chat.completions.create(
         model="qwen-plus",
         messages=[{"role": "user", "content": prompt}],
         temperature=0.75,
     )
     return completion.choices[0].message.content

这是 RAG 的"生成"一环。关键点:

  • Prompt 工程:明确告诉模型"你是 HR 专家 + 只基于给定制度内容作答 + 没有就直说暂无此知识"。

  • 约束 2 极其重要:它克制了模型的幻觉,让回答可追溯、可信。

  • temperature=0.75:适当保留一点表达灵活性,若追求绝对严谨可降到 0.2 左右。

7. 主流程串联

if __name__ == '__main__':
    user_query_text = "员工有哪些权利"
    results = query_by_chroma(user_query_text)
    final_answer = generate_answer(results, user_query_text)
    print(f"大模型生成的最终答案:\n{final_answer}")

首次运行请先取消注释 __main__ 里的入库步骤(load_file → split_text → save_to_vector_db),把员工手册灌进库;之后查询直接走检索 + 生成即可。

五、运行效果

问:员工有哪些权利

系统会:① 把问题向量化 → ② 从 employee_manual_collection 捞出最相关的 5 段手册内容 → ③ 连同问题拼进 Prompt → ④ qwen-plus 基于真实片段作答。

由于 Prompt 里强制"基于制度、无则说暂无",答案可溯源、不乱编

六、生产化进阶建议

维度 当前朴素实现 生产建议
分片 固定 200 字符 递归字符切分 / 语义切分,保留段落边界
表格 未处理 遍历 document.tables 一并向量化
检索 单次向量检索 混合检索(向量 + 关键词 BM25)
重排 加 Cross-Encoder 重排提升 TopK 精度
上下文 直接拼接 加引用来源、显示出处章节
并发 单进程 入库/查询异步化,批量提升吞吐

七、小结

本文用不到 150 行代码,跑通了一个企业级 RAG 问答原型

  • 文档读取python-docx

  • 向量化:通义千问 qwen3.7-text-embedding

  • 向量库ChromaDB 本地持久化

  • 生成qwen-plus + 强约束 Prompt

RAG 的精髓就一句话:让大模型"知之为知之,不知为不知",答案永远有出处。把这个骨架换成你的合同、FAQ、产品文档,就是一个专属知识库问答机器人。


Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐