第 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 processingDAG 跑在自定义 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 的判据和评测集是最先需要重做的三块。
参考链接
- 顶层 README(目录结构、API 端点表、Makefile 命令、环境变量、排障、成本):https://github.com/jamwithai/production-agentic-rag-course/blob/main/README.md
- Airflow 配置与 DAG 说明:https://github.com/jamwithai/production-agentic-rag-course/blob/main/airflow/README.md
- 环境变量完整清单:https://github.com/jamwithai/production-agentic-rag-course/blob/main/.env.example
- 生产 ingestion DAG:https://github.com/jamwithai/production-agentic-rag-course/blob/main/airflow/dags/arxiv_paper_ingestion.py
- 各周 notebook:https://github.com/jamwithai/production-agentic-rag-course/blob/main/notebooks/week1/week1_setup.ipynb、https://github.com/jamwithai/production-agentic-rag-course/blob/main/notebooks/week6/week6_cache_testing.ipynb、https://github.com/jamwithai/production-agentic-rag-course/blob/main/notebooks/week7/week7_agentic_rag.ipynb
- 系统提示词:https://github.com/jamwithai/production-agentic-rag-course/blob/main/src/services/ollama/prompts/rag_system.txt
- Agentic RAG 服务与端点:https://github.com/jamwithai/production-agentic-rag-course/blob/main/src/services/agents/agentic_rag.py、https://github.com/jamwithai/production-agentic-rag-course/blob/main/src/routers/agentic_ask.py
- 对应博客:https://jamwithai.substack.com/p/the-mother-of-ai-project、https://jamwithai.substack.com/p/production-ready-rag-monitoring-and、https://jamwithai.substack.com/p/agentic-rag-with-langgraph-and-telegram
- 对应 release tag:https://github.com/jamwithai/arxiv-paper-curator/releases/tag/week1.0、https://github.com/jamwithai/arxiv-paper-curator/releases/tag/week7.0
release 链接同样出自源材料原文,仓库名与课程主仓库不同,取历史代码状态时按 tag 克隆即可。
到这里,这套系统里能被别人复制的部分和只属于 arXiv 的部分就分开了。分层、编排、缓存、trace、发布入口这些是工程骨架,跟语料无关,可以整块搬走;数据源、解析、mapping、提示词、评测这五块是内容和判断,换领域就得重做一轮。判断一个 RAG 项目是否真的到了生产可用,看的不是它跑通了哪条链路,而是这五块里每一块出了问题,你有没有地方能看见、有指标能判定、有命令能重来。