🚩 Phase 1: MVP 原型验证 (V1.0)

时间:2026-01-13
目标:跑通“爬取 -> 向量化 -> 问答”的全流程,验证最小可行性。

🛠️ 核心实现

  • 后端架构:搭建 FastAPI 服务,打通 /build (单篇构建) 和 /chat (问答) 接口。

  • AI 链路:LangChain 串联了 WebBaseLoaderOpenAIEmbeddingsGPT-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) 策略。

    1. Delete: 处理某 URL 前,先执行 db.delete(where={"source": url}) 清除该文章历史数据。

    2. 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 较高时又会引入大量噪音(如导航栏文本)。

架构演进:

  1. 扩大召回 (Recall): 将初筛数量从 Top 3 扩大到 Top 15。

  2. 混合检索 (Hybrid Search): 手写 SimpleEnsembleRetriever,融合了 BM25 (关键词匹配) 和 Chroma (语义匹配),解决专有名词(如 "1Panel")搜不到的问题。

  3. 精准排序 (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,导致评测脚本刷屏报错。

  • 解法:

    1. 实现 Checkpoint (存档点) 机制:将推理结果实时存入 JSON。如果评测中断,下次运行直接读取存档,无需重新推理(断点续传)。

    2. 增加 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

  • 尝试解决:

    1. Nginx 反向代理: 试图通过二级域名 api.fanfanya.top 统一入口。

    2. SSL 证书申请 (失败):

      • DNS-01 验证: 遭遇阿里云解析同步延迟,1Panel 状态长时间卡在 Waiting

      • HTTP-01 验证: 验证请求被 Nginx 反向代理配置拦截,导致 403 错误。

    3. Cloudflare 代理 (放弃): 尝试通过 CF Flexible 模式实现伪 HTTPS,但由于网络拓扑配置复杂、DNS 服务器迁移成本高等因素,在操作阶段由于疲劳和 ROI(投入产出比)考虑,决定停止尝试。

⚠️ 2. 核心技术债 (Technical Debt)

  • 网络闭环未完成: 目前系统仅能在本地环境(Localhost)或关闭浏览器安全检查的情况下跑通。

  • 运维复杂度低估: 严重低估了从单机 Script 到公网 HTTPS 服务的运维链路复杂度。对于 AI 应用开发者而言,SSL、DNS 劫持及 Nginx 精细化配置是后续必须补齐的短板。


🏆 项目结项总结:Halo 博客 RAG 助手 (Alpha 版)

项目现状:
核心业务逻辑(后端 RAG 链路)已全部闭环,生产环境部署(HTTPS/SSL)因运维障碍暂时停滞。

已实现的技术点 (Done):

  1. 后端架构: 基于 FastAPI 构建了支持异步流式响应(Streaming)的 RAG API。

  2. 数据清洗: 实现了基于 LLM 蒸馏的 Smart ETL,解决了原始 HTML 噪音大、召回率低的问题。

  3. 增量同步: 通过 MD5 内容校验实现了文章的原子化更新,防止向量库冗余。

  4. 检索调优: 手写混合检索算法,兼顾语义与专有名词的召回。

待解决的问题 (Todo):

  1. SSL 自动化: 需进一步理顺反向代理与 .well-known 验证路径的关系。

  2. 全链路加密: 完成从 HTTP 到 HTTPS 的彻底迁移。

  3. 前端 UX: 优化在网络延迟较大时的打字机效果流畅度。

项目反思:
这次实战让我清醒地认识到,AI 开发不等于调 API。一个完整的 AI 产品,80% 的代码和精力都在处理数据清洗、异常捕获、网络安全和运维部署上。虽然最终没能亲手在公网点击那个小机器人,但对 RAG 全链路的理解已经不再流于表面。