1. 概述

本项目基于 Civilopedia 构建《文明6》RAG 问答系统,支持用户询问游戏相关的各类问题。

核心挑战:游戏知识涉及多个领域(伟人、建筑、单位、奇观等),用户问题往往跨领域。例如问"秦始皇的特色单位是什么",需要同时检索"领袖"和"单位"两个知识库。

解决方案:路由判定 + 多路检索。用户提问后,系统先判断涉及哪些知识库,再并行召回、聚合生成。


2. 整体架构

系统分为三大阶段:

阶段职责关键技术
数据采集抓取、解析、入库BeautifulSoup, LangChain
检索生成路由判定、多路召回、生成回答Ollama LLM, Milvus, LangChain
服务部署API 服务化FastAPI, Docker Compose

3. 数据采集层

3.1 采集策略

数据来源为 Civilopedia 网站,共 15 个采集器:

采集器主题说明
fetch_great_person.ipynb伟人采集所有伟人信息
fetch_building.ipynb建筑采集建筑属性
fetch_unit.ipynb单位采集单位属性
fetch_wonder.ipynb奇观采集奇观属性
fetch_religion.ipynb宗教采集宗教信息
fetch_leader.ipynb领袖采集领袖信息
… 共15个

3.2 解析与文档化

每个采集器的工作流程相同:

  1. 构造 URL 列表:根据主题构造待抓取页面 ID 列表
  2. 发送请求:requests.get 抓取页面
  3. 解析 HTML:BeautifulSoup 提取关键字段
  4. 封装 Document:LangChain Document 对象

伟人采集关键代码(fetch_great_person.ipynb):

from bs4 import BeautifulSoup
from langchain_core.documents import Document
import requests

base_url = 'https://www.civilopedia.net/zh-CN/gathering-storm/greatpeople/'
url = f"{base_url}great_person_individual_{person_id}"
response = requests.get(url)
soup = BeautifulSoup(response.content, 'html.parser')

# 提取关键字段
name = soup.find('div', class_='App_pageHeaderText__SsfWm').get_text()
# 提取特色能力、身份等...

doc = Document(
    page_content=f"伟人姓名:{name}\n特色能力:{abilities}\n身份:{duty}",
    metadata={"source": url}
)

3.3 文本分词与入库

from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Milvus
from langchain_ollama import OllamaEmbeddings

# 分词:chunk=1000, overlap=200
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
split_docs = text_splitter.split_documents(docs)

# 向量化入库
embeddings = OllamaEmbeddings(model="embeddinggemma")
vectorstore = Milvus.from_documents(
    split_docs,
    embedding=embeddings,
    collection_name="great_person"
)

4. 向量存储层

4.1 Milvus Collection 设计

Collection存储内容示例字段
belief万神殿信仰信仰名、效果
religion宗教宗教名、信徒领袖
country文明国家文明特性
leader领袖名称、特色能力
building建筑名称、成本、产出
wonder奇观名称、建造条件
resource资源类型、分布
citystate城邦名称、增益类型
district城区名称、建造条件
feature地形地形效果
great_person伟人名称、时代、特长
improvement改良设施名称、建造条件
moment历史时刻名称、触发条件
unit单位名称、强度、升级
unit_promotion晋升名称、效果

4.2 向量化

使用 embeddinggemma 模型进行向量化,存入 Milvus。


5. 检索生成层

这是系统的核心,分为三个步骤:

5.1 路由判定

问题:用户问题可能涉及多个领域,如何确定检索范围?

方案:让 LLM 自主判断,用 Pydantic 模型保证输出稳定。

RouteQuery 定义(server.py):

from pydantic import BaseModel, Field
from typing import List, Literal

class RouteQuery(BaseModel):
    tables: List[Literal[
        "belief", "religion", "country", "leader", "building", "wonder",
        "resource", "citystate", "district", "feature", "great_person",
        "improvement", "moment", "unit", "unit_promotion"
    ]] = Field(description="这个问题与哪些关键字有关,返回对应的表名")

判定流程(server.py):

import os
from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

# 第一次调用:让 LLM 分析问题
llm = ChatOllama(model="gpt-oss:120b-cloud")
first_llm = ChatOllama(model="llama3.1")
prompt = ChatPromptTemplate.from_template(template)
chain = prompt | first_llm | StrOutputParser()
response = chain.invoke({"question": question})

# 第二次调用:结构化输出保证 JSON 稳定
structured_llm = ChatOllama(model="llama3.1")
structured_chain = structured_llm.with_structured_output(RouteQuery)
result = structured_chain.invoke(response)
# result.tables 即确定的检索范围
用户问题判定结果
“秦始皇的特色单位是什么”["leader", "unit"]
“哪些领袖信仰天主教”["religion", "leader"]
“科学类伟人有哪些”["great_person"]

5.2 多路检索

问题:跨领域查询需要同时从多个 Collection 召回。

方案:为每个涉及的 Collection 创建独立检索器,并行执行。

from langchain_core.runnables import RunnableParallel
from langchain_ollama import OllamaEmbeddings
from lib import get_vectorstore_from_milvus

embeddings = OllamaEmbeddings(model="embeddinggemma")

# 为每个涉及的表创建检索器
retriever_dict = {}
for table in result.tables:
    vectorstore = get_vectorstore_from_milvus(
        embeddings,
        collection_name=table,
        connection_args={"host": MILVUS_HOST, "port": MILVUS_PORT}
    )
    retriever_dict[table] = vectorstore.as_retriever(search_kwargs={"k": 100})

# RunnableParallel 并行执行,结果自动聚合
multi_retriever = RunnableParallel(retriever_dict)

5.3 回答生成

原则:严格基于检索结果,禁止编造和省略。

提示词约束(lib.py):

你是一名文明6的专家,根据用户的问题严格回答:
1. 只能使用上下文中的内容,禁止使用外部知识
2. 按 source 逐条列出,不合并
3. 不省略、不推断

RAG Chain(lib.py):

from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import ChatOllama
from langchain_core.output_parsers import StrOutputParser

def generate_answer_by_multiple_retriever(question, multiple_retriever, llm):
    template = """..."""

    prompt = ChatPromptTemplate.from_template(template)

    rag_chain = (
        RunnableParallel({
            "context": multiple_retriever,
            "question": RunnablePassthrough(),
        })
        | prompt
        | llm
        | StrOutputParser()
    )
    return rag_chain.invoke(question)

6. 服务接口

FastAPI 提供单一端点(server.py):

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class AskRequest(BaseModel):
    question: str

class AskResponse(BaseModel):
    answer: str

@app.post("/ask", response_model=AskResponse)
def ask(req: AskRequest):
    answer = generate_answer(req.question.strip())
    return AskResponse(answer=answer)

调用示例

// POST /ask
// 请求
{ "question": "哪些领袖信仰天主教" }

// 响应
{ "answer": "..." }

7. 部署架构

Docker Compose 编排四个服务:

服务镜像端口职责
app自定义8081RAG API
ollamaollama/ollama:0.13.011434LLM 推理 + Embedding
milvusmilvusdb/milvus:v2.6.419530向量数据库
attuzilliz/attu:v2.68000Milvus 管理界面

核心环境变量

变量说明
CIVI6_OLLAMA_HOSTOllama 服务地址
MILVUS_HOSTMilvus 服务地址
MILVUS_PORTMilvus 端口

8. 设计亮点

设计意图
路由判定按需检索,避免全量扫描,节省资源
结构化输出Pydantic + 二次调用确保 JSON 稳定
多路并行RunnableParallel 实现真正并行
严格回答提示词约束保证答案准确性
按需扩展新增领域只需新建 Collection