🚩 Phase 1: MVP 原型验证 (V1.0)
时间:2026-01-13
目标:跑通“爬取 -> 向量化 -> 问答”的全流程,验证最小可行性。
🛠️ 核心实现
后端架构:搭建 FastAPI 服务,打通 /build (单篇构建) 和 /chat (问答) 接口。
AI 链路:LangChain 串联了 WebBaseLoader → OpenAIEmbeddings → GPT-5。
存储方案:使用 FAISS (CPU版)。
决策理由:轻量级,无依赖,适合快速开发 Demo。
容器化:编写 Dockerfile,解决 1Panel 部署时的 OOM 问题(配置 Swap 分区)。
🐛 踩坑与复盘
网络障碍:服务器无法拉取 Docker 镜像和调用 OpenAI API。
解决:配置 ShellCrash 代理及 Docker 镜像源。
遗留痛点:数据易失性。FAISS 是内存数据库,容器重启后知识库清零,无法满足生产需求。
🚀 Phase 2: 数据持久化与自动化 (V1.1)
时间:2026-01-14 (Today)
目标:解决“重启即焚”问题,并实现全站文章自动同步。
🔄 架构重构 (Refactoring)
1. 存储层升级:FAISS → ChromaDB
变更动作:弃用内存型向量库 FAISS,迁移至支持本地存储的 ChromaDB。
工程落地:
代码配置 persist_directory="./chroma_db" 实现数据落盘。
Docker 优化:引入 Volume (数据卷) 挂载,将容器内的 /app/chroma_db 映射到宿主机,确保即使删除容器,数据依然保留。
学习收获:掌握了 Docker 容器生命周期中“计算与存储分离”的设计原则。
2. ETL 管道自动化:Sitemap 爬虫
变更动作:新增 /build_all 接口,替代手动输入 URL。
技术实现:
使用 xml.etree.ElementTree 解析 Halo 博客的 sitemap.xml。
自动提取所有文章链接,批量调用爬虫和向量化逻辑。
成果:实现了**“一键全站学习”**,极大降低了运维成本。
🐛 深度问题分析 (Deep Dive)
RAG 的“统计幻觉”问题
现象:全站爬取成功后,问 AI “一共有几篇文章”,AI 无法回答或胡编乱造。
深度分析 (面试点):RAG 本质是语义检索 (Semantic Search) 而非 SQL 查询。检索器只能根据“一共有几篇”这句话找到语义相似的片段,而无法对数据库进行全局计数 Count(*)。
验证方案:改为询问新文章中特有的知识点(如“Halo 部署用了什么面板?”),AI 回答正确,证明全站爬取成功。
⚠️ 技术债 (Tech Debt)
数据幂等性问题:目前每次调用 /build_all,都会生成新的向量数据(内容相同但 ID 不同)。多次构建会导致数据库膨胀,且检索结果会出现重复内容,影响回答质量。
🛡️ Phase 2.5: 数据管道深度优化 (V1.2)
时间:2026-01-15
目标:解决Sitemap解析的网络问题,清洗脏数据,并实现“原子性”文章更新。
🛠️ 核心功能演进 (Evolution)
1. 网络适配与容错 (Network Resilience)
问题场景:Halo 博客配置了外部端口 :8090,但 Python 容器网络无法访问该非标准端口,导致 Sitemap 解析连接被重置。
解决方案:在 ETL 流程中增加 URL 清洗逻辑。
代码实现:url.strip().replace(":8090", ""),强制回退到标准 80 端口进行抓取。
效果:成功连通博客服务器,Sitemap 解析恢复正常。
2. 智能数据清洗 (Signal-to-Noise Ratio Optimization)
问题分析:Sitemap 中包含 /tags/, /categories/, /archives, /about 等归档页。这些页面主要是链接列表,缺乏实质性文本。若存入向量库,会成为检索噪音(Noise),干扰 RAG 准确性。
优化动作:实现白名单过滤机制。
逻辑:解析 XML 后,剔除包含排除词(如 categories)或非 HTTP 协议的链接。
成果:从抓取 8 个链接(含噪音)精准收敛为 2 篇核心文章,显著提升了知识库的信噪比。
3. 工业级数据同步策略 (Delete-then-Insert) 🔥
背景:原计划仅使用 MD5(URL+Content) 生成 ID 进行去重。
缺陷发现:简单的 ID 去重无法处理文章修改的场景。若文章内容变更,ID 随之改变,新切片插入,但旧切片(旧 ID)仍残留在库中,导致“幽灵数据”。
最终方案:实施 “先删后写” (Atomic Sync) 策略。
Delete: 处理某 URL 前,先执行 db.delete(where={"source": url}) 清除该文章历史数据。
Insert: 爬取新内容,重新切片并存入。
价值 (面试杀手锏):确保了向量库与博客内容的强一致性,彻底解决了版本冲突问题。
💻 关键代码对比 (Code Refactoring)
Before (V1.1 - 简单的追加模式)
codePython
# 缺点:文章改了,旧数据还赖在数据库里
vectorstore.add_documents(docs)After (V1.2 - 同步模式)
codePython
# 优点:保证数据库永远只有最新版本,无冗余
# 1. 精确打击:删除当前 URL 的所有旧切片
db.delete(where={"source": url})
# 2. 生成带唯一 ID 的新切片
chunk_id = md5(url + content)
# 3. 写入
db.add_documents(documents=new_chunks, ids=[chunk_id])🔮 Phase 3: 工程化重构与混合检索实战 (V1.3)
时间: 2026-01-21
目标: 将项目从“能跑的脚本”升级为“工业级工程”,解决专有名词检索不准的问题,并修复复杂的依赖冲突。
🏗️ 1. 架构重构:从 Script 到 Clean Architecture
随着功能增加,扁平的文件结构(所有文件都在根目录)变得难以维护。为了符合 FastAPI 的最佳实践,我对项目进行了彻底的分层重构。
变更前 (Before): 乱成一团,配置、逻辑、接口混在一起。
变更后 (After): 采用了标准的 app/ 目录结构:
codeText
BIOG_AI_BOT/
├── .env # 环境变量
├── .gitignore # 屏蔽 __pycache__ 和 data
├── run.py # 统一启动入口
└── app/ # 📦 核心代码
├── core/ # ⚙️ 配置与安全 (Config, Security)
├── api/v1/ # 🔌 接口层 (Endpoints)
├── services/ # 🧠 业务逻辑 (RAG, Crawler)
└── schemas/ # 📝 数据模型 (Pydantic)工程亮点:
路径解耦: 摒弃了硬编码路径(如 data\db),改用 pathlib 动态获取根目录,彻底解决了 Windows 开发、Linux Docker 部署 时的路径不兼容问题。
配置集中: 所有环境变量(API Key, DB Path)收敛至 app.core.config.Settings,实现单点管理。
🔐 2. 安全性升级:API 鉴权
痛点: 之前的接口处于“裸奔”状态,任何人只要知道 IP 就能消耗我的 Token。
方案: 引入 FastAPI 的 Depends 依赖注入机制。
实现了 fanfanya 请求头验证。
在 endpoints.py 中挂载全局依赖,非法请求直接拦截返回 403 Forbidden。
🔎 3. 核心突破:混合检索 (Hybrid Search)
痛点: 纯向量检索(Dense Retrieval)对语义理解很好,但对“非语义的专有名词”(如 ShellCrash, 1Panel, v1.2)召回率极低。
方案: 引入 BM25(关键词匹配) + Vector(向量匹配) 的加权融合策略。
技术实现难点 (Data Rehydration):
BM25 算法是基于内存的,需要全量文本统计词频。而我的数据持久化在 ChromaDB 中。
解决: 在初始化检索器时,先从 Chroma 中 get() 出所有 Document 对象,动态构建 BM25 索引。
策略: 设置 weights=[0.5, 0.5],兼顾语义理解与精确匹配。
💥 4. 填坑实录:手撕 Retriever 绕过依赖地狱
这是本阶段最大的挑战。
问题现场:
由于 LangChain 版本迭代(v0.3 vs v1.x),EnsembleRetriever 的引用路径在 langchain, langchain-community, langchain-core 之间反复横跳。
即便反复重装环境,依然报错:ImportError: cannot import name 'EnsembleRetriever'。
工程化解法 (Bypass Strategy):
与其在复杂的版本依赖中内耗,不如直接源码级集成。
我阅读了 LangChain 源码,发现 EnsembleRetriever 的核心逻辑只是简单的 RRF (Reciprocal Rank Fusion) 加权。于是我在 app/services/rag.py 中手动实现了一个轻量级的混合检索类:
codePython
# 核心逻辑复刻
class SimpleEnsembleRetriever(BaseRetriever):
def _get_relevant_documents(self, query: str, ...):
# 1. 并行调用 Vector 和 BM25 检索
# 2. 结果去重合并
# 3. 返回 Top-K
...价值: 这种“不被库限制死”的能力,是比调包更重要的工程素养。
📉 Phase 4: 评测驱动开发 (EDD) 与工程化填坑实录 (V1.4)
时间: 2026-01-22
核心目标: 拒绝“体感测试”,建立基于 Ragas 的自动化评估流水线;引入 Hybrid Search (混合检索) 与 Rerank (重排序) 试图提升精度,最终通过数据量化揭示了系统的真实缺陷。
📊 1. 建立黄金评估标准 (The Golden Standard)
背景:
之前的开发全是靠“我觉得准”。但在面试中,无法量化的优化是毫无说服力的。我决定引入 LLM-as-a-Judge(以大模型为裁判)机制。
技术实现:
构建黄金数据集 (Golden Dataset):
基于博客文章,人工精编了 14 条高难度问答对 (tests/golden_dataset.json)。
覆盖维度: 涵盖了 DevOps (Docker/ShellClash)、后端 (FastAPI/CORS)、AI 原理 (Embedding/Fine-tuning)。
难度升级: 不仅包含 Simple(事实类)问题,特意加入了 Reasoning(推理类)问题。例如:“为什么 OOM?”——模型必须理解“配置低 -> 需要 Swap”的因果链条。
评测架构:
框架: Ragas。
指标: 锁定 Context Recall (召回率 - 东西找全了吗?) 和 Faithfulness (忠实度 - 没瞎编吧?)。
🏗️ 2. 检索链路升级:Rerank 与 混合检索
痛点: 简单的向量检索 (Vector Search) 在 Top-K 较低时容易漏掉正确答案,Top-K 较高时又会引入大量噪音(如导航栏文本)。
架构演进:
扩大召回 (Recall): 将初筛数量从 Top 3 扩大到 Top 15。
混合检索 (Hybrid Search): 手写 SimpleEnsembleRetriever,融合了 BM25 (关键词匹配) 和 Chroma (语义匹配),解决专有名词(如 "1Panel")搜不到的问题。
精准排序 (Precision): 接入 Jina Rerank API (Cross-Encoder),对 15 个候选文档进行二次语义打分,只取前 3 名喂给大模型。
🐛 3. 爬虫与网络的“填坑”史诗 (Troubleshooting Log)
今天下午经历了三次严重的工程阻断,每一次都对应一个经典的面试考点。
坑位一:Sitemap 爬取的 403 Forbidden
现象: 使用 requests 爬取博客 Sitemap 时,被 Cloudflare/Nginx 拦截。
尝试: 伪装 User-Agent,甚至引入 curl_cffi 模拟 Chrome 指纹。
最终解法 (Service Degradation): 由于 Windows 本地环境 SSL 握手库冲突 (SSLEOFError),采取了服务降级策略——暂时回退到 HTTP IP 直连模式,优先保证数据入库流程跑通。(工程启示:在非核心路径上,可用性优于完美性。)
坑位二:API 限流导致的“自杀式”评测
现象: Ragas 默认并发请求,瞬间触发了中转 API 的 429 Too Many Requests,导致评测脚本刷屏报错。
解法:
实现 Checkpoint (存档点) 机制:将推理结果实时存入 JSON。如果评测中断,下次运行直接读取存档,无需重新推理(断点续传)。
增加 asyncio.sleep 缓冲,以时间换空间。
坑位三:致命的“逻辑误杀” (The Silent Killer) 🔥
现象: 评测初期,Context Recall 为 0。
排查: 发现 Jina Rerank API 在高并发下频发 10054 Connection Reset。
根因: 代码中存在一个逻辑漏洞——当 Rerank API 失败触发降级(返回 0 分)时,下游逻辑设置了 if score < 0.2: continue 的硬阈值。这导致在网络抖动时,所有检索到的正确文档被代码主动丢弃,导致上下文为空。
修复: 移除了硬性阈值过滤,优先保证有内容输入。
📉 4. 惨痛的评测结果与反思
最终战报:
Context Recall: 0.0641 (极低)
Faithfulness: 0.68
Answer Relevancy: 0.23
深度归因 (Why?):
并没有像预想那样获得高分。通过分析日志 (tests/evaluation_report.csv),发现了一个 RAG 系统最常见的问题——非结构化数据的“信噪比”极低。
含码量过高: 我的博客是实战教程,包含大量 Python/YAML 代码块。
语义错位: 当问题是“Embedding 模型选了什么”时,向量检索找到的是 class RAGService... 等代码定义。虽然关键词匹配(都有 Embedding 字样),但Ragas 裁判模型(GPT-5)无法从代码片段中提取出自然语言答案,因此判定召回失败。
💡 Phase 4 总结
今天的实战证明了:算法(Rerank)救不了烂数据。
目前的“暴力全站爬取”策略,对于技术博客这种由“代码+文本”混合的场景效果极差。
Next Step (Phase 5):
必须从“模型调优”转向**“数据工程”**。下一步计划放弃直接爬取 HTML,而是实施 ETL 清洗,手动构建结构化的 FAQ 知识库,将代码噪音剥离,用高质量数据重构系统。
⚗️ Phase 5: 数据炼金术与流式响应 (V1.5)
时间: 2026-01-25
核心目标: 既然 Phase 4 证明了“算法救不了烂数据”,本阶段彻底转型 Data-Centric AI(以数据为中心),利用 LLM 蒸馏清洗数据;同时解决接口超时与用户体验卡顿的问题。
🔥 1. 核心重构:Smart ETL (智能蒸馏流水线)
痛点 (Pain Point):
直接切片 (Chunking) 策略导致信噪比极低。导航栏、CSS 代码、无关的侧边栏文本被切入向量库,严重干扰了语义检索,导致 Phase 4 的召回率仅为 0.06。解决方案 (Synthetic Data Engineering):
放弃机械切片,引入 LLM-based ETL。动作: 在入库前,调用 GPT-4o/DeepSeek 扮演“技术编辑”,阅读原始 HTML,忽略代码细节,提取核心 Q&A 对。
存储变更:
page_content: 存“问题” (用于高精度语义匹配)。
metadata: 存“答案” (用于生成)。
成果:
知识库从“碎尸万段的 HTML 片段”变成了“高质量的专家问答集”。
💰 2. 成本控制:MD5 增量更新 (Incremental Update)
缺陷发现:
引入 LLM 蒸馏后,构建成本飙升(Token 消耗)。每次点击“重建”,所有文章都会重新跑一遍 LLM,既慢又烧钱。工程落地:
实现了基于 Content Hash 的幂等性检查。逻辑: Current_Hash = MD5(HTML_Content)。
比对: 入库前先查 ChromaDB,如果 Old_Hash == Current_Hash,直接 return,跳过昂贵的 LLM 调用。
代码对比 (Code Diff):
codePython
# Before: 傻瓜式覆盖 await smart_etl.extract_qa(full_text) # After: 带脑子的更新 current_hash = hashlib.md5(full_text.encode()).hexdigest() if old_hash == current_hash: logger.info(f"⏭️ 内容未变,跳过蒸馏: {url}") return
⚡ 3. 性能优化:异步队列与并发控制
崩溃现场:
全站爬取时,并发调用 LLM 瞬间触发 API 的 429 Too Many Requests,且长连接导致 Nginx 超时 (504 Gateway Time-out)。架构演进:
Semaphore (信号量): 限制 smart_etl 内部同时只有 3 个 LLM 请求在跑。
BackgroundTasks: 将 /build_all 接口改造为“发令枪”。前端请求立刻返回 Processing,耗时的爬虫任务丢入 FastAPI 后台线程池运行。
🌊 4. 用户体验:流式响应 (Streaming)
痛点:
RAG 链路太长(检索+重排+推理),用户提问后需要干等 5-8 秒才能看到答案,体验极差。技术实现:
后端: 使用 Python 生成器 (yield) + StreamingResponse,配合 LangChain 的 astream 接口。
前端: 改造 Fetch API,使用 ReadableStream 解码,实现 ChatGPT 同款打字机效果。
面试杀手锏: 显著降低了 TTFB (Time To First Byte),将首字延迟从 5s 压缩至 0.5s。
📉 Phase 6: 生产环境部署的“最后一公里”折磨与中断 (V1.6 - Suspended)
时间: 2026-01-27
状态: 已挂起 (Blocked)
目标: 解决公网 HTTPS 环境下的全链路闭环。
🛠️ 1. 遭遇的问题:Mixed Content 墙
现状: 博客主站为 https://fanfanya.top,后端 API 为 http://101.132.236.9:8088。
现象: 浏览器强制拦截非加密请求,导致前端无法通过 fetch 获取后端数据,报错 Mixed Content。
尝试解决:
Nginx 反向代理: 试图通过二级域名 api.fanfanya.top 统一入口。
SSL 证书申请 (失败):
DNS-01 验证: 遭遇阿里云解析同步延迟,1Panel 状态长时间卡在 Waiting。
HTTP-01 验证: 验证请求被 Nginx 反向代理配置拦截,导致 403 错误。
Cloudflare 代理 (放弃): 尝试通过 CF Flexible 模式实现伪 HTTPS,但由于网络拓扑配置复杂、DNS 服务器迁移成本高等因素,在操作阶段由于疲劳和 ROI(投入产出比)考虑,决定停止尝试。
⚠️ 2. 核心技术债 (Technical Debt)
网络闭环未完成: 目前系统仅能在本地环境(Localhost)或关闭浏览器安全检查的情况下跑通。
运维复杂度低估: 严重低估了从单机 Script 到公网 HTTPS 服务的运维链路复杂度。对于 AI 应用开发者而言,SSL、DNS 劫持及 Nginx 精细化配置是后续必须补齐的短板。
🏆 项目结项总结:Halo 博客 RAG 助手 (Alpha 版)
项目现状:
核心业务逻辑(后端 RAG 链路)已全部闭环,生产环境部署(HTTPS/SSL)因运维障碍暂时停滞。
已实现的技术点 (Done):
后端架构: 基于 FastAPI 构建了支持异步流式响应(Streaming)的 RAG API。
数据清洗: 实现了基于 LLM 蒸馏的 Smart ETL,解决了原始 HTML 噪音大、召回率低的问题。
增量同步: 通过 MD5 内容校验实现了文章的原子化更新,防止向量库冗余。
检索调优: 手写混合检索算法,兼顾语义与专有名词的召回。
待解决的问题 (Todo):
SSL 自动化: 需进一步理顺反向代理与 .well-known 验证路径的关系。
全链路加密: 完成从 HTTP 到 HTTPS 的彻底迁移。
前端 UX: 优化在网络延迟较大时的打字机效果流畅度。
项目反思:
这次实战让我清醒地认识到,AI 开发不等于调 API。一个完整的 AI 产品,80% 的代码和精力都在处理数据清洗、异常捕获、网络安全和运维部署上。虽然最终没能亲手在公网点击那个小机器人,但对 RAG 全链路的理解已经不再流于表面。