第 10 章 RAG工程化配置

第 10 章:收尾:工程化清单与把案例迁移到自己的领域

第 10 章:收尾:工程化清单与把案例迁移到自己的领域

前面九章按周推进,读的时候容易只记住某个技术点,忘了整套系统长什么样。这一章反过来做一件事:不引入新概念,把 arXiv 论文助手当成一个已经上线的项目来盘一遍——目录怎么分层、端点有哪些、命令怎么敲、需要申请哪些 key、出问题先看哪里、钱花在哪。盘完这些,再回答最后一个问题:如果我的语料不是 arXiv 论文,这套代码里哪些部件是可以照搬的,哪些必须换掉。

目录结构与分层

顶层 README 给出的项目树如下:

arxiv-paper-curator/
├── src/                    # Main application code
│   ├── routers/            # API endpoints (search, ask, papers)
│   ├── services/           # Business logic (opensearch, ollama, agents, cache)
│   ├── models/             # Database models (SQLAlchemy)
│   ├── schemas/            # Pydantic validation schemas
│   └── config.py           # Environment configuration
├── notebooks/              # Weekly learning materials (week1-7)
├── airflow/                # Workflow orchestration (DAGs)
├── tests/                  # Test suite
└── compose.yml             # Docker service orchestration

五层职责不重叠,这是这套代码最容易迁移的部分。routers/ 只做 HTTP 出入参,检索、生成、缓存的逻辑都在 services/ 里;数据库表结构在 models/(SQLAlchemy),请求与响应体在 schemas/(Pydantic),两者分开意味着改表不必改接口契约;config.py 统一读环境变量,业务代码不直接碰 os.environ。services/ 往下按能力再分一层,README 里出现过的子目录有 opensearch/、embeddings/、indexing/text_chunker.py、ollama/、cache/、langfuse/、agents/nodes/、telegram/——每一周新增的能力都是往这一层挂一个新目录,前面的没被重写。

airflow/ 是独立的一套东西,有自己 README:

airflow/
├── README.md                           # This file
├── Dockerfile                          # Custom Airflow container with dependencies
├── requirements-airflow.txt            # Python dependencies for DAGs
└── dags/
    ├── hello_world_dag.py             # Week 1 health check DAG
    ├── arxiv_paper_ingestion.py       # Week 2 production ingestion DAG
    └── arxiv_ingestion/
        └── tasks.py                   # Production pipeline tasks with async processing

DAG 跑在自定义 Airflow 容器里,用 requirements-airflow.txt 单独装依赖,而不是和 API 共用一份依赖表。调度侧的依赖和在线服务侧的依赖生命周期不同,分开是对的。生产 DAG arxiv_paper_ingestion 的步骤是固定的七步:环境校验与缓存初始化、抓取前一天论文(默认 10 篇)、Docling 解析 PDF、失败重试、写 PostgreSQL、OpenSearch 占位(README 里标注 Week 3+ 才接真索引)、生成日报。

一处口径差异记一下:项目结构写作 compose.yml,而 Week 6 的架构清单里写的是 docker-compose.yml。docker compose 两个文件名都会自动识别,不影响使用,但你要去找文件时得知道有这两种写法。

API 端点总表

README 的 "API Endpoints Reference" 给的是这张表:

Endpoint Method Description Week
/health GET Service health check Week 1
/api/v1/papers GET List stored papers Week 2
/api/v1/papers/{id} GET Get specific paper Week 2
/api/v1/search POST BM25 keyword search Week 3
/api/v1/hybrid-search/ POST Hybrid search (BM25 + Vector) Week 4

这张表停在 Week 4,后面几周加的端点没有并进来。按各周 README 的描述,src/routers/ask.py 提供 /api/v1/ask 与 /api/v1/stream 一对端点,src/routers/agentic_ask.py 提供 POST /api/v1/ask-agentic,.env 里还有 TELEGRAM__WEBHOOK_PATH 默认的 /telegram/webhook。要一份完整的端点清单,以 /docs 上 FastAPI 自动生成的为准,文档页不会漏。同样地,健康检查在 Quick Start 里写成 curl http://localhost:8000/api/v1/health,而表里写的是 /health——两条路径在实际项目里都存在过,排障时都试一下比纠结哪个才对更快。

Makefile 命令全集与直接命令的对应

README 推荐走 Makefile,并给全了命令:

Command Description
make start Start all services
make stop Stop all services
make restart Restart all services
make status Show service status
make logs Show service logs
make health Check all services health
make setup Install Python dependencies
make format Format code
make lint Lint and type check
make test Run tests
make test-cov Run tests with coverage
make clean Clean up everything

不想用 Makefile 时,README 的 "Direct Commands" 段落明确给了四组对应:

Makefile Direct
make status docker compose ps
make logs docker compose logs
make test uv run pytest
make start docker compose up --build -d

其余命令的直接形态源材料没有逐条展开:make setup 走的是 uv sync(Quick Start 与 Week 7 的克隆流程都用它装依赖),make stop / make clean 对应 docker compose down 系列,克隆 release 时官方流程用的是 docker compose down -v 再 up --build -d。这几条是从上下文对齐出来的,make help 打印的才是 Makefile 里真实的定义。有 Makefile 的意义不在于少敲几个字,而在于把"我本地是这么跑的"固化成团队里唯一一份可复现的入口。

环境变量与要申请的 key

README 单独列了三个必须自己申请的凭据,并标了生效周次:

变量 必要性 用途
JINA_API_KEY Week 4+ 必需 Embedding 生成(Hybrid Search 的向量侧)
TELEGRAM__BOT_TOKEN Week 7 必需 Telegram bot 接入
LANGFUSE__PUBLIC_KEY / LANGFUSE__SECRET_KEY Week 6 可选 RAG 链路 trace

按 README 的说法,.env.example 里的默认值开箱可用,OpenSearch、arXiv API 与服务间连接都不需要改;Jina 的 key 是免费申请的,Langfuse 可以起本地实例。Airflow 侧另有一组(来自 airflow/README.md):

AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=postgresql+psycopg2://rag_user:rag_password@postgres:5432/rag_db
AIRFLOW__CORE__EXECUTOR=LocalExecutor
POSTGRES_DATABASE_URL=postgresql+psycopg2://rag_user:rag_password@postgres:5432/rag_db
PYTHONPATH=/opt/airflow/src

变量名里的双下划线是层级分隔的约定(AIRFLOW__CORE__EXECUTOR),和 .env.example 的组织方式对应。PYTHONPATH=/opt/airflow/src 这一行决定了 DAG 能不能 import 到 src/ 下的服务代码,容器里路径写错,DAG 会在解析阶段就红。Airflow 的登录用户名密码是容器初始化时自动生成的,README 指明去 airflow/simple_auth_manager_passwords.json.generated 取。那份 DSN 里带着 rag_user:rag_password 这样的默认口令,本地开发无所谓,对外部署前必须换掉,.env 也不能带着真 token 进版本库。

测试、代码质量与故障排查

开发工具链在 README 的技术栈里写得很短:UV、Ruff、MyPy、Pytest、Docker Compose。对应的入口就是四条命令——make format 格式化、make lint 做 lint 与类型检查、make test 跑测试、make test-cov 带覆盖率跑测试;绕过 Makefile 就是 uv run pytest,依赖安装统一走 uv sync。

单元测试之外,这个项目实际上有两套验收机制。一是每周一个 notebook,路径固定为 notebooks/week1/week1_setup.ipynb 到 notebooks/week7/week7_agentic_rag.ipynb,README 为每一周都写了 "Completion Guide"。这些 notebook 不是教程演示,它们承担的是端到端验收:服务起没起来、索引有没有数据、检索结果对不对、缓存有没有命中,都在里面查。二是运行时观测面——API 的 /docs、Gradio(7861)、Langfuse(3000)、Airflow(8080)、OpenSearch Dashboards(5601)。回归测试跑绿不代表链路通,Langfuse 里看不到 trace 就说明埋点那段没被执行到。Week 7 加的 Agentic 链路尤其要注意,它多了一个决策维度:测试断言的对象不该只是答案文本,而应该是决策路径。

链路跑通之后,日常打交道最多的就是这张排障表。README 的 Troubleshooting 给了三条最常见的问题,Airflow README 补充了容器侧的实现细节,Week 7 补充了 Telegram 与缓存的检查方法:

症状 先看什么
服务起不来 先等 2-3 分钟(README 给的建议),再 docker compose logs 看具体哪个容器
端口冲突 8000(API)、8080(Airflow)、5432(PostgreSQL)、9200(OpenSearch)是否已被占用
内存不足 提高 Docker Desktop 的内存分配;前置要求是 8GB+ RAM、20GB+ 可用磁盘
Airflow 登录不上 用户名密码在 airflow/simple_auth_manager_passwords.json.generated
DAG 找不到 src 检查 PYTHONPATH=/opt/airflow/src 与 ../src 的挂载
Telegram bot 无响应 TELEGRAM__ENABLED 与 TELEGRAM__BOT_TOKEN;日志用 docker compose logs -f api | grep telegram
回答慢 首次慢是正常的,docker exec rag-redis redis-cli ping 应返回 PONG;确认缓存与 trace 没在阻塞
权限/挂载报错 Airflow 容器以 airflow 用户(50000:0)运行,日志走 named volume,避免 bind mount 权限冲突

彻底重置的官方命令是:

docker compose down --volumes && docker compose up --build -d

--volumes 会连数据卷一起删,PostgreSQL 里的论文和 OpenSearch 里的索引都会没,重来时 ingestion DAG 得重跑。排查顺序上,先确认容器健康再怀疑代码:make health 一次把所有服务的状态打出来,比逐个猜快得多。Airflow 侧还有一层自己的重试逻辑(arXiv API 的 3 秒延迟与失败重试),日志里的失败未必是代码 bug,可能只是 arXiv 那边限流了。

成本结构

README 只给了一个成本口径,原文写得很保守:课程本身完全免费,本地开发 $0,如果选择外部 LLM 服务,可选的云 API 开销大约 $2-5。这是官方 README 的口径,不是实测账单,原文也没有标注时间单位,引用时别把它当成月度预算。真要算账,金额之外的两项反而更硬:前置要求 8GB+ RAM 和 20GB+ 可用磁盘,以及 Telegram Bot API 本身免费无配额。

这套设计把成本压在本地是因为整条链路默认跑 Ollama 本地推理、Jina 免费 key、Langfuse 本地实例、Redis 本地容器。换成云 LLM 之后,Week 6 的缓存就有了新的意义:README 把缓存收益口径写作 150-400x 提速,Week 7 的基准表里对应的是首次查询 15-20s、命中缓存 50-100ms 两行——省的不只是用户的等待,还有重复调用 LLM 的 token 与费用。缓存命中率越高,账单越接近 $0;反过来说,唯一查询占比高的场景(比如用户总在问新论文)这套估算不成立。

迁移到自己领域:该换的五个部件

把 arXiv 论文助手搬到自己的语料上,可以照搬的是骨架:分层结构、Docker Compose 编排、FastAPI + /docs、Airflow DAG 的形态、Redis 缓存、Langfuse trace、Telegram 入口、Makefile 与测试流程。要动的是内容和内容相关的判断逻辑,集中在五处:

部件 现在是什么 换的时候连带要改什么
数据源 ArxivClient,带 rate limiting 与 retry;DAG 每天抓 CS.AI 论文,默认 10 篇 换成你的 API 或库表时要重新定分页与限流语义;DAG 的 schedule 与"前一天"这个窗口是同源的,改窗口就要改增量判断
解析器 PDFParserService,Docling 加 Tesseract OCR、Poppler 换成 HTML/Markdown/工单/表格解析后,解析产物必须落成同一套 metadata 字段,否则 papers 表与 OpenSearch mapping 都得跟着改
索引 mapping OpenSearch 2.19,mapping 里带分类、年份等字段供 filter 与 boost 使用 字段名与类型是你的领域决定的;改 mapping 要重建索引并重跑 ingestion,线上不能只改代码
Chunking 与 Embedding src/services/indexing/text_chunker.py 的 section-aware 切分加 overlap,依赖论文的小节结构;向量由 Jina 生成 换 embedding 模型会改向量维度,mapping 里的维度必须同步,且全量重算;换切分策略等于换召回质量,要重跑评测
提示词与判据 src/services/ollama/prompts/rag_system.txt,以及 agents/prompts.py 里的 guardrail、grade、rewrite 域边界、相关性判据、改写方向三处都是领域相关的:guardrail 判"不在我范围内"、grade 判"这批结果够不够用"、rewrite 往哪个方向改,全都要按你的语料重写并配合评测集验证
评测 Week 3 引入的 precision、recall、relevance 三个指标,加上每周一个 notebook 的人工验收 需要自己的标注集;Agentic 链路的断言对象换成 reasoning_steps 里的决策路径与 retrieval_attempts,因为答案文本会漂,决策路径才稳定

有一类部件不用动:缓存 key 的构造与 TTL 语义、Langfuse 的 trace/span 分层、Telegram 的命令集与设置项、Docker 与 Makefile。它们跟语料无关,跟"这套系统要被谁怎么用"有关。反过来,决定迁移工作量的也是同一件事——如果新领域的文档结构越接近学术论文(有明确小节、有标题元数据、问题以概念解释为主),五个部件里能照搬的越多;如果是客服工单或代码库这类结构松散、答案依赖时效的语料,chunking、grade 的判据和评测集是最先需要重做的三块。

参考链接

release 链接同样出自源材料原文,仓库名与课程主仓库不同,取历史代码状态时按 tag 克隆即可。

到这里,这套系统里能被别人复制的部分和只属于 arXiv 的部分就分开了。分层、编排、缓存、trace、发布入口这些是工程骨架,跟语料无关,可以整块搬走;数据源、解析、mapping、提示词、评测这五块是内容和判断,换领域就得重做一轮。判断一个 RAG 项目是否真的到了生产可用,看的不是它跑通了哪条链路,而是这五块里每一块出了问题,你有没有地方能看见、有指标能判定、有命令能重来。