在前面的阶段中,我完成了数据清洗、嵌入模型微调、对话模型 LoRA 微调、本地意图识别、FastAPI 服务封装以及 Node.js 集成。今天记录的是最后两个关键阶段:思考过程可视化(流式 SSE) 和 独立 RAG 知识库构建。
阶段 5:思考过程可视化(流式 + 前端时间轴)
目标
让用户看到“小甘草”的推理步骤(意图识别 → 知识检索 → 回答生成),并将最终答案逐字输出,提升透明度和信任感。
后端实现:Server‑Sent Events (SSE)
在 rag_query.py 中新增一个生成器函数 query_tcm_stream,它会在每个关键步骤 yield 一个事件,并在生成回答时逐字输出 token。
python
def query_tcm_stream(question, top_k=3):
# 1. 意图识别步骤
yield {"type": "step", "step": "intent", "content": "正在识别用户意图..."}
intent = classify_intent(question)
yield {"type": "step", "step": "intent", "content": f"意图识别完成:{intent}"}
if intent != "medical":
final = "请描述具体的症状或问题,我会尽力为您分析。"
for ch in final:
yield {"type": "token", "content": ch}
yield {"type": "done"}
return
# 2. 检索步骤
yield {"type": "step", "step": "retrieve", "content": "正在检索相关知识库..."}
q_emb = embed_model.encode(question).tolist()
results = collection.query(query_embeddings=[q_emb], n_results=top_k)
context = "\n\n".join(results['documents'][0])
yield {"type": "step", "step": "retrieve", "content": f"检索到 {len(results['documents'][0])} 条相关知识"}
# 3. 生成步骤
yield {"type": "step", "step": "generate", "content": "正在生成回答..."}
# ... 调用对话模型生成 answer(省略模板代码)
for ch in answer:
yield {"type": "token", "content": ch}
yield {"type": "done"}
然后在 api.py 中增加一个 SSE 路由,将生成器的输出包装为 text/event-stream 响应:
python
from fastapi.responses import StreamingResponse
import json
@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
async def event_generator():
for event in query_tcm_stream(req.message):
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
return StreamingResponse(event_generator(), media_type="text/event-stream")
注意:SSE 原生只支持 GET,这里为了保持与原有接口风格一致使用了 POST。前端不能直接用
EventSource,需要改用fetch+ReadableStream读取。也可以将接口改为 GET,参数放在 query string 中。
前端组件:ReactSteps.vue
创建一个 Vue 组件,通过 fetch 请求流式接口,逐步解析数据并更新界面。
vue
<template>
<div class="steps-container">
<div v-for="(step, idx) in steps" :key="idx" class="step-card">
<div class="step-icon">{{ step.icon }}</div>
<div class="step-content">
<div class="step-title">{{ step.title }}</div>
<div class="step-detail">{{ step.content }}</div>
</div>
</div>
<div v-if="finalAnswer" class="final-answer">
<div class="answer-title">✨ 小甘草的回答 ✨</div>
<div class="answer-text">{{ finalAnswer }}</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const steps = ref([])
const finalAnswer = ref('')
export async function startStream(userMessage) {
steps.value = []
finalAnswer.value = ''
const response = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: 'web', message: userMessage })
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n\n')
buffer = lines.pop()
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6))
if (data.type === 'step') {
const icon = data.step === 'intent' ? '🧠' : (data.step === 'retrieve' ? '📚' : '💬')
const title = data.step === 'intent' ? '意图识别' : (data.step === 'retrieve' ? '知识检索' : '回答生成')
steps.value.push({ icon, title, content: data.content })
} else if (data.type === 'token') {
finalAnswer.value += data.content
} else if (data.type === 'done') {
return
}
}
}
}
}
</script>
使用时,在聊天页面中导入组件并调用 startStream 即可。
阶段 6:独立 RAG 知识库(ChromaDB + 自训嵌入模型)
目标
除了使用清洗后的 shennong_pro.json 构建的主向量库,再增加一个可插拔的独立知识库,用于存放额外的中医文档(TXT/PDF),方便以后扩展知识来源。
步骤一:准备文档
在项目根目录下创建 data/tcm_knowledge/ 文件夹,放入任意数量的 .txt 或 .pdf 文件(例如《中医基础理论》节选)。
步骤二:编写构建脚本 build_knowledge_base.py
使用 LangChain 的文档加载器、文本分割器和 Chroma 向量库。
python
from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_huggingface import HuggingFaceEmbeddings
KNOWLEDGE_DIR = "data/tcm_knowledge"
PERSIST_DIR = "./chroma_knowledge_db"
EMBED_MODEL_PATH = "./models/tcm_embedding_pro"
docs = []
txt_loader = DirectoryLoader(KNOWLEDGE_DIR, glob="**/*.txt", loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"})
docs.extend(txt_loader.load())
# 如果有 PDF,同样加载
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = text_splitter.split_documents(docs)
embedding = HuggingFaceEmbeddings(model_name=EMBED_MODEL_PATH)
vectorstore = Chroma.from_documents(chunks, embedding, persist_directory=PERSIST_DIR)
vectorstore.persist()
运行 python build_knowledge_base.py 后,会在当前目录生成 chroma_knowledge_db 文件夹。
步骤三:实现检索函数
在 rag_query.py 末尾添加:
python
from langchain_community.vectorstores import Chroma
from langchain_huggingface import HuggingFaceEmbeddings
_knowledge_db = None
def get_knowledge_db():
global _knowledge_db
if _knowledge_db is None:
_knowledge_db = Chroma(
persist_directory="./chroma_knowledge_db",
embedding_function=HuggingFaceEmbeddings(model_name="./models/tcm_embedding_pro")
)
return _knowledge_db
def search_tcm_knowledge(query: str, k: int = 3):
db = get_knowledge_db()
results = db.similarity_search(query, k=k)
return [doc.page_content for doc in results]
步骤四:测试与调优
创建一个测试脚本 test_knowledge.py:
python
from rag_query import search_tcm_knowledge
query = "气虚怎么调理"
docs = search_tcm_knowledge(query)
for i, doc in enumerate(docs):
print(f"结果{i+1}:\n{doc}\n{'-'*50}")
运行后观察返回的文档块是否相关。如果不理想,可以调整 chunk_size(例如改为 300)和 chunk_overlap(例如 50),重新构建向量库。
效果与总结
- 阶段 5 完成后,用户可以在前端看到“意图识别 → 知识检索 → 回答生成”的实时步骤,并且最终答案逐字显示,交互体验大幅提升。
- 阶段 6 提供了一个独立的知识库,可以随时补充新的中医文档,并通过
search_tcm_knowledge函数轻松集成到主 RAG 流程中(例如作为检索工具或备用知识源)。
至此,“小甘草”的核心功能已经全部实现,接下来是部署到服务器或继续优化检索质量。