在探索大语言模型(LLM)应用落地的过程中,你是否遇到过这样的困境:团队内部积累了大量关于模型使用、API调用、最佳实践和故障排查的文档,但它们散落在各个聊天记录、个人笔记和Confluence页面中?当新成员加入或遇到一个棘手的问题时,往往需要花费大量时间进行“知识考古”。一个集中、智能、易于查询的内部知识库,成为了提升团队协作与研发效率的刚需。本文将为你详细介绍如何构建一个“托管式LLM Wiki”——一个专为技术团队设计的,基于现代Web技术栈,并能与LLM深度集成的知识管理解决方案。无论你是想快速搭建一个轻量级文档站,还是希望打造一个能通过自然语言交互的智能知识中枢,这里都有从零到一的完整实践路径。
1. 背景与核心概念:为什么需要LLM Wiki?
在深入技术细节之前,我们有必要厘清几个核心概念以及这个项目所要解决的根本问题。
1.1 LLM与知识管理的碰撞
大语言模型(LLM)如GPT系列、Claude、通义千问等,已经展现出强大的自然语言理解和生成能力。它们不再是遥不可及的实验室产物,而是逐渐渗透到代码生成、文档撰写、问题解答等日常开发环节中的实用工具。
然而,LLM的“通用知识”与团队的“私有知识”之间存在鸿沟。LLM可能不知道你公司内部特定的API规范、项目独有的架构设计决策,或是上周刚修复的那个诡异Bug的解决方案。LLM Wiki的核心思想,就是构建一个桥梁,将LLM的能力与团队内部的私有、结构化知识结合起来。
1.2 什么是Hosted LLM Wiki?
“Hosted”意为“托管的”,它强调了这个Wiki系统的部署和运维特性。一个Hosted LLM Wiki通常包含以下关键特征:
- 私有化部署:代码和数据掌握在自己手中,部署在团队可控的服务器或云环境,保障了知识资产的安全与隐私。
- Wiki核心功能:提供完整的知识创建、编辑、组织、版本控制和搜索功能,就像Confluence或MediaWiki一样。
- LLM深度集成:这不是简单的“给Wiki加个聊天机器人”。集成是双向的:
- 知识库增强LLM(RAG):Wiki作为向量知识库,当用户提问时,系统先从中检索最相关的文档片段,再连同问题和片段一起提交给LLM,生成基于内部知识的精准回答。这就是检索增强生成(RAG)的核心流程。
- LLM赋能Wiki管理:利用LLM自动为文档生成摘要、标签,甚至辅助编写和校对内容,提升知识运营的效率。
1.3 核心应用场景与价值
- 团队内部知识沉淀:统一存放所有项目文档、技术规范、会议纪要和事故复盘报告。
- 高效智能问答:新同事可以直接提问“我们项目如何连接数据库?”或“处理支付回调的注意事项是什么?”,系统能基于内部文档给出准确回答,极大降低培训成本。
- 开发支持:集成到IDE或命令行工具中,快速查询某个库的内部使用示例、某个微服务的接口定义。
- 客户支持:构建面向外部用户的智能帮助中心,基于产品文档自动解答常见问题。
接下来,我们将从技术选型开始,一步步搭建一个具备上述能力的系统。
2. 技术选型与环境准备
构建一个LLM Wiki涉及前端、后端、向量数据库、LLM接口等多个层面。以下是一个经过验证的、平衡了功能与复杂度的技术栈方案。
2.1 技术栈说明
- 前端 Wiki 界面:
Wiki.js。它是一个现代、开源、基于Node.js的Wiki系统,界面美观,支持Markdown、可视化编辑,权限管理完善,且API友好,易于集成。 - 后端与向量化服务:
Python+FastAPI。Python是AI生态的首选语言。FastAPI能快速构建高性能的RESTful API,用于处理文档向量化、检索和与LLM的交互。 - 向量数据库:
Chroma或Qdrant。两者都是轻量级、易于使用的开源向量数据库。Chroma更简单,适合快速入门;Qdrant性能更强,功能更丰富。本文示例使用Chroma。 - 嵌入模型:
text-embedding-ada-002(OpenAI) 或开源模型如BAAI/bge-small-zh-v1.5。负责将文本转换为向量。为演示方便,我们使用OpenAI的嵌入API。 - 大语言模型:
GPT-3.5-turbo或GPT-4(OpenAI API),或开源模型如Qwen、ChatGLM的API。本文使用OpenAI API进行演示。 - 部署与容器:
Docker&Docker Compose。用于容器化所有服务,实现一键部署和环境统一。
2.2 环境与版本说明
请确保你的开发或服务器环境满足以下要求。版本号是关键,不匹配可能导致依赖冲突。
- 操作系统:Linux (Ubuntu 20.04/22.04 LTS 推荐) 或 macOS。Windows建议使用WSL2。
- Docker:版本 20.10.0 或更高。
- Docker Compose:版本 1.29.0 或更高。
- Python:版本 3.9 或 3.10(用于后端服务开发)。
- Node.js:Wiki.js需要,但Docker镜像中已包含,无需本地安装。
重要:你需要准备一个有效的OpenAI API密钥,并确保有足够的额度。
2.3 项目结构预览
在开始之前,我们先规划整个项目的目录结构,这有助于理解各个组件的关系。
hosted-llm-wiki/ ├── docker-compose.yml # 主部署文件 ├── wiki-js/ # Wiki.js 配置目录 │ ├── config.yml # Wiki.js 配置文件 │ └── data/ # Wiki.js 数据卷 (挂载) ├── backend/ # 后端向量化与RAG服务 │ ├── Dockerfile │ ├── requirements.txt │ ├── app/ │ │ ├── main.py # FastAPI 主应用 │ │ ├── chroma_client.py # 向量数据库客户端 │ │ ├── embedding.py # 嵌入模型调用 │ │ └── llm_client.py # LLM调用封装 │ └── data/ # 向量数据库持久化目录 (挂载) └── .env.example # 环境变量示例文件3. 部署Wiki.js作为知识管理前端
我们首先部署Wiki.js,它将成为我们知识库的“门面”和内容管理核心。
3.1 使用Docker Compose快速部署
创建docker-compose.yml文件,这是所有服务的编排定义。
version: '3.8' services: wiki-js-db: image: postgres:15-alpine container_name: llm-wiki-db environment: POSTGRES_DB: wiki POSTGRES_USER: wikijs POSTGRES_PASSWORD: your_secure_db_password_here # 务必修改! volumes: - wiki-db-data:/var/lib/postgresql/data restart: unless-stopped networks: - llm-wiki-network wiki-js: image: ghcr.io/requarks/wiki:2.5 container_name: llm-wiki-frontend depends_on: - wiki-js-db environment: DB_TYPE: postgres DB_HOST: wiki-js-db DB_PORT: 5432 DB_USER: wikijs DB_PASS: your_secure_db_password_here # 与上面一致 DB_NAME: wiki volumes: - ./wiki-js/data:/var/wiki/data # 持久化上传文件等 - ./wiki-js/config.yml:/var/wiki/config.yml # 挂载自定义配置 ports: - "3000:3000" # 将容器3000端口映射到主机3000端口 restart: unless-stopped networks: - llm-wiki-network # 后端RAG服务将在下一节添加 # chroma-db: # ... networks: llm-wiki-network: driver: bridge volumes: wiki-db-data:关键配置解释:
wiki-js-db:使用PostgreSQL作为Wiki.js的数据库。volumes:将数据库数据(wiki-db-data)和Wiki.js配置(config.yml)、用户数据(data)持久化到宿主机,避免容器重启后数据丢失。networks:创建一个独立的Docker网络llm-wiki-network,让服务间能通过服务名互相访问。
3.2 配置Wiki.js
创建wiki-js/config.yml文件。这是一个最简化的配置,用于连接数据库。
# Wiki.js 配置文件 port: 3000 bind: 0.0.0.0 db: type: postgres host: wiki-js-db port: 5432 user: wikijs pass: your_secure_db_password_here db: wiki ssl: false logLevel: info dataPath: ./data3.3 启动并初始化Wiki.js
- 在项目根目录(
hosted-llm-wiki)下,运行命令启动服务:docker-compose up -d - 等待几十秒后,在浏览器中访问
http://你的服务器IP:3000。 - 你将看到Wiki.js的安装向导。大部分配置已通过
config.yml和环境变量完成,向导页面通常只需要你设置管理员账号(邮箱和密码)。请务必记住这个账号。 - 完成设置后,登录系统。你现在拥有了一个功能完整的Wiki站点!可以开始创建页面、编写文档了。
4. 构建后端RAG服务与集成LLM
现在,我们构建核心的“智能”部分——一个能够读取Wiki内容、进行向量化存储,并响应智能问答的后端服务。
4.1 创建后端服务项目结构
进入backend目录,创建必要的文件。
1. 定义依赖 (requirements.txt):
fastapi==0.104.1 uvicorn[standard]==0.24.0 openai==1.3.0 chromadb==0.4.18 langchain==0.0.340 python-dotenv==1.0.0 pydantic==2.5.0 requests==2.31.0 beautifulsoup4==4.12.2 # 可选,用于解析HTML内容2. 编写Dockerfile (backend/Dockerfile):
FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY ./app ./app CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]3. 创建应用核心代码: 首先,创建环境变量管理文件.env(从.env.example复制并填写):
OPENAI_API_KEY=sk-your-openai-api-key-here OPENAI_EMBEDDING_MODEL=text-embedding-ada-002 OPENAI_LLM_MODEL=gpt-3.5-turbo CHROMA_PERSIST_DIRECTORY=/app/data/chroma_db WIKI_JS_API_BASE=http://wiki-js:3000 WIKI_JS_API_TOKEN=your_wiki_js_api_token # 需要在Wiki.js后台生成注意:WIKI_JS_API_TOKEN需要在Wiki.js管理后台的API Access页面生成。
4. 实现向量数据库客户端 (backend/app/chroma_client.py):
import chromadb from chromadb.config import Settings import os from typing import List from .embedding import get_embedding_function class ChromaClient: def __init__(self, persist_directory: str): # 创建持久化客户端 self.client = chromadb.PersistentClient( path=persist_directory, settings=Settings(anonymized_telemetry=False) # 禁用匿名数据收集 ) # 获取或创建集合(类似于数据库的表) # 使用我们自定义的嵌入函数 self.collection = self.client.get_or_create_collection( name="wiki_knowledge", embedding_function=get_embedding_function() ) def add_documents(self, documents: List[str], metadatas: List[dict], ids: List[str]): """向向量库添加文档""" if documents: self.collection.add( documents=documents, metadatas=metadatas, ids=ids ) print(f"Added {len(documents)} documents to Chroma.") def query(self, query_text: str, n_results: int = 5) -> List[dict]: """查询最相关的文档""" results = self.collection.query( query_texts=[query_text], n_results=n_results ) # 格式化返回结果 retrieved_docs = [] if results['documents']: for i, doc in enumerate(results['documents'][0]): retrieved_docs.append({ 'content': doc, 'metadata': results['metadatas'][0][i], 'distance': results['distances'][0][i] }) return retrieved_docs def delete_all(self): """清空集合(用于测试或重置)""" self.client.delete_collection(name="wiki_knowledge") self.collection = self.client.get_or_create_collection( name="wiki_knowledge", embedding_function=get_embedding_function() )5. 实现嵌入函数 (backend/app/embedding.py):
from chromadb import EmbeddingFunction, Embeddings import openai import os from typing import List class OpenAIEmbeddingFunction(EmbeddingFunction): def __init__(self): api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise ValueError("OPENAI_API_KEY environment variable is not set") self.client = openai.OpenAI(api_key=api_key) self.model = os.getenv("OPENAI_EMBEDDING_MODEL", "text-embedding-ada-002") def __call__(self, input: List[str]) -> Embeddings: # 调用OpenAI Embedding API response = self.client.embeddings.create( model=self.model, input=input ) # 提取嵌入向量 embeddings = [item.embedding for item in response.data] return embeddings def get_embedding_function(): """返回嵌入函数实例""" return OpenAIEmbeddingFunction()6. 实现LLM客户端 (backend/app/llm_client.py):
import openai import os from typing import List, Dict, Any class LLMClient: def __init__(self): api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise ValueError("OPENAI_API_KEY environment variable is not set") self.client = openai.OpenAI(api_key=api_key) self.model = os.getenv("OPENAI_LLM_MODEL", "gpt-3.5-turbo") def generate_response(self, query: str, context: List[Dict[str, Any]]) -> str: """基于检索到的上下文生成回答""" # 构建系统提示词,指导LLM如何利用上下文 system_prompt = """你是一个专业的助手,专门回答基于提供的内部知识库的问题。 请严格根据以下提供的上下文信息来回答问题。如果上下文信息不足以回答问题,请如实说明“根据现有知识库无法回答此问题”,不要编造信息。 上下文信息: """ # 将检索到的上下文拼接起来 context_text = "\n\n".join([f"[来源:{doc['metadata'].get('title', '未知')}]\n{doc['content']}" for doc in context]) user_prompt = f"问题:{query}\n\n请根据以上上下文回答。" messages = [ {"role": "system", "content": system_prompt + context_text}, {"role": "user", "content": user_prompt} ] try: response = self.client.chat.completions.create( model=self.model, messages=messages, temperature=0.1, # 低温度,使输出更确定,更依赖上下文 max_tokens=1000 ) return response.choices[0].message.content except Exception as e: return f"调用LLM时发生错误:{str(e)}"7. 实现主API应用 (backend/app/main.py):
from fastapi import FastAPI, HTTPException, BackgroundTasks from pydantic import BaseModel from typing import List, Optional import os from dotenv import load_dotenv from .chroma_client import ChromaClient from .llm_client import LLMClient import requests import json # 加载环境变量 load_dotenv() app = FastAPI(title="Hosted LLM Wiki Backend", description="RAG服务后端API") # 初始化客户端 chroma = ChromaClient(os.getenv("CHROMA_PERSIST_DIRECTORY", "/app/data/chroma_db")) llm_client = LLMClient() # 数据模型定义 class QueryRequest(BaseModel): question: str top_k: Optional[int] = 5 class IndexRequest(BaseModel): page_id: Optional[str] = None # 如果为空,则同步所有页面 force: Optional[bool] = False class QAResponse(BaseModel): question: str answer: str sources: List[dict] # 辅助函数:从Wiki.js API获取页面内容 def fetch_wiki_page(page_id: str = None): """从Wiki.js获取单个页面或所有页面内容""" base_url = os.getenv("WIKI_JS_API_BASE") token = os.getenv("WIKI_JS_API_TOKEN") headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } if page_id: # 获取单个页面 url = f"{base_url}/api/pages/{page_id}" else: # 获取所有页面(可能需要分页,这里简化处理) url = f"{base_url}/api/pages" try: response = requests.get(url, headers=headers, timeout=30) response.raise_for_status() return response.json() except Exception as e: print(f"Error fetching from Wiki.js: {e}") return None def process_and_index_page(page_data): """处理页面数据并索引到向量数据库""" # 简化处理:提取标题和内容 # 实际应用中,可能需要解析Markdown/HTML,进行更精细的文本分割(chunking) page_id = page_data.get('id') title = page_data.get('title', 'Untitled') content = page_data.get('content', '') # 可能是Markdown或HTML # 简单的文本分割:按段落或固定长度分割 # 这里为了演示,将整个页面内容作为一个文档块 documents = [content] metadatas = [{ "page_id": page_id, "title": title, "source": "wiki-js", "url": f"/{page_id}" # Wiki.js页面路径 }] ids = [f"wiki_page_{page_id}"] chroma.add_documents(documents, metadatas, ids) return True # API端点 @app.post("/index", status_code=202) async def index_pages(req: IndexRequest, background_tasks: BackgroundTasks): """触发知识库索引(异步)""" background_tasks.add_task(run_indexing, req.page_id, req.force) return {"message": "索引任务已开始在后台运行"} def run_indexing(page_id: str = None, force: bool = False): """实际执行索引的后台任务""" print(f"开始索引Wiki页面, page_id: {page_id}, force: {force}") if force: chroma.delete_all() print("已清空现有向量库。") pages_data = fetch_wiki_page(page_id) if not pages_data: print("无法从Wiki.js获取数据。") return # 处理返回的数据结构 if page_id: # 单个页面 if process_and_index_page(pages_data): print(f"成功索引页面: {pages_data.get('title')}") else: # 多个页面 if isinstance(pages_data, list): for page in pages_data: if process_and_index_page(page): print(f"成功索引页面: {page.get('title')}") else: print("获取到的页面数据格式不符合预期。") print("索引任务完成。") @app.post("/query", response_model=QAResponse) async def query_knowledge_base(req: QueryRequest): """查询知识库并获取智能回答""" if not req.question or req.question.strip() == "": raise HTTPException(status_code=400, detail="问题不能为空") # 1. 检索相关文档 retrieved_docs = chroma.query(req.question, n_results=req.top_k) if not retrieved_docs: return QAResponse( question=req.question, answer="知识库中暂无相关信息。", sources=[] ) # 2. 调用LLM生成回答 answer = llm_client.generate_response(req.question, retrieved_docs) # 3. 整理来源信息 sources = [] for doc in retrieved_docs: sources.append({ "title": doc['metadata'].get('title', '未知标题'), "page_id": doc['metadata'].get('page_id'), "relevance_score": 1 - doc['distance'] # 简单转换,距离越小越相关 }) return QAResponse( question=req.question, answer=answer, sources=sources ) @app.get("/health") async def health_check(): """健康检查端点""" return {"status": "healthy", "service": "llm-wiki-backend"} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)4.2 更新Docker Compose以集成后端服务
现在,将后端服务和ChromaDB添加到docker-compose.yml中。
version: '3.8' services: wiki-js-db: # ... 保持不变 ... wiki-js: # ... 保持不变 ... chroma-db: # 注意:ChromaDB以服务模式运行,但我们的客户端使用持久化模式。 # 这里我们仅作为独立服务运行,实际存储使用本地卷。 image: chromadb/chroma:0.4.18 container_name: llm-wiki-chroma command: uvicorn chromadb.app:app --reload --workers 1 --host 0.0.0.0 --port 8001 ports: - "8001:8001" # 暴露端口,可用于管理或其它客户端连接 volumes: - ./backend/data/chroma_db:/chroma/chroma_db # 持久化向量数据 environment: - IS_PERSISTENT=TRUE - PERSIST_DIRECTORY=/chroma/chroma_db restart: unless-stopped networks: - llm-wiki-network rag-backend: build: ./backend container_name: llm-wiki-backend depends_on: - chroma-db environment: - OPENAI_API_KEY=${OPENAI_API_KEY:-your_key_here} - OPENAI_EMBEDDING_MODEL=${OPENAI_EMBEDDING_MODEL:-text-embedding-ada-002} - OPENAI_LLM_MODEL=${OPENAI_LLM_MODEL:-gpt-3.5-turbo} - CHROMA_PERSIST_DIRECTORY=/app/data/chroma_db - WIKI_JS_API_BASE=http://wiki-js:3000 - WIKI_JS_API_TOKEN=${WIKI_JS_API_TOKEN:-your_token_here} volumes: - ./backend/data:/app/data # 挂载数据卷,使向量库持久化 - ./backend/app:/app/app # 开发时挂载代码,生产环境可移除 ports: - "8000:8000" # 后端API端口 restart: unless-stopped networks: - llm-wiki-network networks: llm-wiki-network: driver: bridge volumes: wiki-db-data:关键点:
chroma-db服务:我们运行了Chroma的服务实例,但我们的Python客户端(chromadb.PersistentClient)使用的是本地文件模式。这里运行服务主要是为了演示另一种连接方式,并确保环境一致。数据通过卷./backend/data/chroma_db持久化。rag-backend服务:构建我们的FastAPI应用。它依赖chroma-db服务(虽然未直接使用其HTTP接口),并挂载了相同的向量数据卷。环境变量通过${VARIABLE_NAME}语法从宿主机环境或.env文件读取。- 重要:在运行前,需要在项目根目录创建
.env文件,并填入正确的OPENAI_API_KEY和WIKI_JS_API_TOKEN。
4.3 启动完整系统并测试
生成Wiki.js API Token:
- 登录Wiki.js管理后台(
http://localhost:3000)。 - 进入
管理->API访问。 - 点击
生成新令牌,赋予它读取页面的权限,复制生成的令牌。 - 将令牌填入项目根目录的
.env文件的WIKI_JS_API_TOKEN变量中。
- 登录Wiki.js管理后台(
在Wiki.js中创建一些测试页面,例如:
首页:欢迎页面。项目部署指南:描述如何部署项目的Markdown文档。API规范:描述团队内部API设计规范的文档。
启动所有服务:
# 在项目根目录执行 docker-compose down # 如果之前启动过,先停止 docker-compose up -d --build # --build 会重新构建后端镜像使用
docker-compose logs -f rag-backend查看后端日志,确保启动无误。触发知识库索引: 我们的Wiki内容已经更新,需要将其同步到向量数据库。调用后端索引API:
# 同步所有页面 curl -X POST http://localhost:8000/index \ -H "Content-Type: application/json" \ -d '{"force": false}'如果返回
{"message": "索引任务已开始在后台运行"},则成功。查看后端容器日志,确认索引过程。测试智能问答API:
curl -X POST http://localhost:8000/query \ -H "Content-Type: application/json" \ -d '{"question": "如何部署这个项目?", "top_k": 3}'你应该会得到一个JSON响应,包含基于你刚创建的
项目部署指南页面内容生成的答案,以及引用的来源信息。
5. 前端集成与功能扩展
至此,核心后端服务已就绪。一个完整的系统还需要一个便于用户交互的前端。这里提供两种集成思路:
5.1 方案一:在Wiki.js页面中嵌入问答组件(推荐)
利用Wiki.js支持自定义JavaScript和HTML嵌入的特性,我们可以在Wiki页面内直接集成一个问答窗口。
在Wiki.js中创建一个新页面,例如叫做
智能问答。切换到“源代码”编辑器,插入以下HTML/JS代码:
<div id="llm-qa-container"> <h3>🤖 智能知识库问答</h3> <p>基于本Wiki所有内容进行智能回答。</p> <textarea id="question-input" placeholder="请输入你的问题..." rows="3" style="width:100%; padding: 10px; margin-bottom: 10px;"></textarea> <button onclick="askQuestion()" style="padding: 10px 20px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">提问</button> <div id="answer-area" style="margin-top: 20px; padding: 15px; border: 1px solid #ddd; border-radius: 5px; min-height: 100px; background-color: #f9f9f9;"> <p>答案将显示在这里...</p> </div> <div id="sources-area" style="margin-top: 15px; font-size: 0.9em; color: #666;"></div> </div> <script> async function askQuestion() { const questionInput = document.getElementById('question-input'); const answerArea = document.getElementById('answer-area'); const sourcesArea = document.getElementById('sources-area'); const question = questionInput.value.trim(); if (!question) { alert('请输入问题'); return; } answerArea.innerHTML = '<p><em>思考中...</em></p>'; sourcesArea.innerHTML = ''; try { const response = await fetch('http://你的后端服务器IP:8000/query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question: question, top_k: 3 }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // 显示答案 answerArea.innerHTML = `<p><strong>答案:</strong></p><div>${data.answer.replace(/\n/g, '<br>')}</div>`; // 显示来源 if (data.sources && data.sources.length > 0) { let sourcesHtml = '<p><strong>参考来源:</strong></p><ul>'; data.sources.forEach(source => { // 假设source.url是Wiki.js内的相对路径 sourcesHtml += `<li><a href="${source.url}" target="_blank">${source.title}</a> (相关性: ${(source.relevance_score * 100).toFixed(1)}%)</li>`; }); sourcesHtml += '</ul>'; sourcesArea.innerHTML = sourcesHtml; } } catch (error) { console.error('Error:', error); answerArea.innerHTML = `<p style="color: red;">请求失败: ${error.message}</p>`; } } </script>注意:将代码中的
http://你的后端服务器IP:8000替换为你实际的RAG后端服务地址。如果Wiki.js和后端在同一台机器且通过Docker网络通信,这里可以写http://rag-backend:8000,但需要Wiki.js容器能解析此服务名(它们在同一Docker网络llm-wiki-network中)。更通用的做法是使用宿主机的公网IP或域名,并确保端口可访问。保存页面。现在,访问这个
智能问答页面,你就可以直接在Wiki内部进行提问了。
5.2 方案二:构建独立的问答Web应用
如果你需要一个更独立、功能更复杂的界面(例如支持对话历史、多轮追问),可以单独构建一个前端应用(使用Vue/React),通过调用http://localhost:8000/queryAPI 来获取答案。这超出了本文范围,但架构是清晰的。
6. 常见问题与排查思路
在搭建和使用过程中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| Wiki.js 无法启动,数据库连接失败 | 1. PostgreSQL容器未启动或启动慢。 2. config.yml或环境变量中的数据库密码错误。3. 网络配置问题,容器间无法通信。 | 1. 运行docker-compose logs wiki-js-db查看数据库日志。2. 检查 docker-compose.yml和config.yml中的密码是否一致。3. 确认所有服务都在同一个Docker网络 ( llm-wiki-network) 中。 |
访问http://localhost:3000超时或拒绝连接 | 1. Wiki.js容器未成功启动。 2. 端口 3000被宿主机其他程序占用。 | 1.docker-compose ps查看服务状态,docker-compose logs wiki-js查看日志。2. 使用 netstat -tuln | grep :3000检查端口占用,或修改docker-compose.yml中的端口映射(如"8080:3000")。 |
后端RAG服务启动失败,提示ModuleNotFoundError | 1.requirements.txt中的包未正确安装。2. Docker构建缓存问题。 | 1. 进入后端容器docker exec -it llm-wiki-backend bash,手动pip list检查。2. 使用 docker-compose build --no-cache rag-backend重新构建镜像。 |
调用/indexAPI 后,日志显示无法从Wiki.js获取数据 | 1. Wiki.js API Token 无效或权限不足。 2. 后端服务无法访问Wiki.js的地址 ( WIKI_JS_API_BASE)。3. Wiki.js服务本身未运行。 | 1. 在Wiki.js后台重新生成Token并更新.env文件,重启后端服务。2. 在后端容器内执行 curl http://wiki-js:3000/health测试连通性。3. 确保 WIKI_JS_API_BASE的值在容器网络内可访问(使用服务名wiki-js)。 |
调用/queryAPI 返回答案,但答案质量差,或回答“无法回答” | 1. 索引未成功运行,向量库为空。 2. 文档分割策略不佳,检索不到有效上下文。 3. LLM提示词 ( system_prompt) 需要优化。4. OpenAI API 调用失败或额度不足。 | 1. 检查索引任务的日志,确认页面内容已成功添加。 2. 优化 process_and_index_page函数,实现更智能的文本分割(如按标题、按固定长度重叠分割)。3. 调整 llm_client.py中的system_prompt,使其更明确地要求基于上下文回答。4. 检查OpenAI API密钥和额度。 |
| 嵌入或LLM调用速度慢 | 1. 网络延迟(访问OpenAI API)。 2. 文档块 ( chunk) 太大或太多。3. 未使用批处理。 | 1. 考虑使用国内可访问的LLM/嵌入模型API,或部署开源模型。 2. 优化文本分割,控制每个chunk的大小(如300-500字)。 3. 在 add_documents时,可以考虑批量处理,减少API调用次数。 |
7. 最佳实践与进阶优化建议
将系统运行起来只是第一步,要使其在生产环境中稳定、高效、易用,还需要考虑以下方面:
7.1 知识库内容管理
- 规范化文档结构:在Wiki.js中建立统一的页面模板和分类(如
/技术文档/、/业务规范/、/运维手册/),便于管理和检索。 - 定期同步与增量更新:目前的
/indexAPI 是全量同步。应实现增量索引,监听Wiki.js的webhook(如果支持),或在页面保存时触发单个页面的重新索引。 - 文档预处理与清洗:在索引前,应去除Markdown/HTML中的无关标签、代码块(除非代码是知识的一部分)、图片链接等,保留纯文本核心内容。
7.2 检索增强生成(RAG)优化
- 智能文本分割:不要将整页文档作为一个块。使用
langchain的RecursiveCharacterTextSplitter等工具,按语义(如标题)或固定长度进行重叠分割,能显著提升检索精度。# 示例:使用Langchain进行文本分割 from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, length_function=len, ) docs = text_splitter.create_documents([full_text]) - 元数据丰富:为每个文本块添加丰富的元数据,如所属页面、章节标题、标签、最后修改时间等。这有助于在检索时进行过滤和排序。
- 混合检索:结合向量检索(语义相似度)和关键词检索(如BM25),可以兼顾语义匹配和精确术语匹配,效果往往更好。
- 重排序:初步检索出N个结果(如20个)后,使用一个更精细的模型(重排序器)对它们进行再次排序,只将最相关的几个(如3个)送入LLM生成答案,可以降低成本并提升答案质量。
7.3 系统性能与可观测性
- API限流与认证:为后端
/queryAPI 添加速率限制和API密钥认证,防止滥用。 - 异步处理:索引大量文档时,使用
Celery或RQ等任务队列进行异步处理,避免HTTP请求超时。 - 日志与监控:记录所有查询和索引操作,监控API响应时间、Token消耗、错误率等指标。
- 缓存策略:对常见问题的答案进行缓存(如使用Redis),可以极大减少对LLM和向量数据库的调用,提升响应速度。
7.4 安全与权限
- 环境变量管理:切勿将API密钥等敏感信息硬编码在代码中。使用
.env文件,并在生产环境中使用安全的密钥管理服务(如Vault、云厂商的密钥管理)。 - 网络隔离:确保RAG后端、向量数据库等核心服务不直接暴露在公网。通过反向代理(如Nginx)暴露必要的API,并设置防火墙规则。
- Wiki.js权限继承:目前我们的RAG服务能读取所有Wiki页面。在实际应用中,问答的权限应与Wiki.js的用户权限对齐。这需要更复杂的集成,例如在查询时传递用户身份,并在检索前后进行内容过滤。
7.5 成本控制
- 使用开源模型:将嵌入模型和LLM替换为本地部署的开源模型(如使用
sentence-transformers库的模型,或部署Qwen、ChatGLM等),可以彻底消除API调用成本,并保障数据隐私。这需要更强的GPU算力支持。 - 优化提示词:精心设计提示词,让LLM的回答更简洁、精准,减少不必要的Token消耗。
- 设置用量告警:如果使用商用API,务必在云平台设置用量和费用告警。
通过以上步骤,你已经成功搭建了一个功能完整的、托管式的LLM Wiki系统。它不仅仅是一个静态的知识仓库,更是一个能够理解团队内部知识并与之智能对话的“活”系统。从简单的文档管理到深度的智能问答,这个框架为你提供了坚实的基础,你可以根据团队的具体需求,在检索质量、用户体验、系统架构等方面进行持续的迭代和优化。