• Claude Code + 通义千问,从零搭出生产级 RAG 要花多少钱?


    项目概述

    本项目是一个生产级的 RAG(Retrieval-Augmented Generation,检索增强生成)平台。用户可上传 PDF、Word、Excel、HTML、Markdown 等格式的文档,系统自动完成解析、分块、向量嵌入、索引存储,然后通过对话界面进行智能问答,回答基于上传的文档内容并附带引用来源。

    系统截图

    rag1

    rag2

    rag3

    rag4

    开发方式与成本

    这个项目是怎么做出来的?

    流程分两步,先有方案文档,再有代码

    第一步:生成方案文档 hashed-gliding-metcalfe.md

    真正动手写代码之前,先用 Claude Code 产出一份完整的实现方案(文件名是 Claude Code 自动分配的任务代号)。这份约 440 行的 Markdown 相当于整个项目的「施工蓝图」,内容包括:

    • 生产级 RAG 的架构图与模块划分
    • Monorepo 目录结构与各文件职责
    • LangGraph 10 节点状态机、API 路由、数据模型
    • 混合检索、HyDE、自纠正、评估面板等技术选型

    第二步:按方案文档生成代码

    随后以 hashed-gliding-metcalfe.md 为唯一规格说明,让 Claude Code 逐模块生成 monorepo 代码、Docker 配置、前后端实现;运行报错时再在对话中调试、重构。运行时调用 阿里百炼 / 通义千问 的 OpenAI 兼容 API:

    • 对话与生成qwen3.6-plus
    • 向量嵌入text-embedding-3-large(1024 维,经百炼接口调用)

    花了多少钱?

    全程 API 调用(Embedding 批量写入 + 多轮对话测试 + Rerank 等)合计约 100 元人民币

    为什么要做这个实验?

    主要目的不是「做一个 Demo」,而是实测:从零完成一套可上线、带评测与可观测性的生产级 RAG,究竟要烧多少 Token、踩多少坑。本文把架构选型、14 个 Bug 的排查过程、以及 Token 成本体感一并记录下来,供你在立项或选型时参考——若你也用 Claude Code + 国内 LLM API 走类似路线,可以据此估算预算与工期。

    执行构建流程

    从「写方案」到「跑起来」,整条链路可以概括为下面这张图:上半段是 AI 生成与调试,下半段是本地执行构建

    报错

    通过

    开始

    Claude Code 生成方案文档
    hashed-gliding-metcalfe.md

    按方案逐模块生成代码
    Monorepo · Docker · 前后端 · LangGraph

    本地运行是否通过?

    Claude Code 对话调试
    逐条修复 Bug · 重构

    进入本地执行构建

    docker compose up -d
    Milvus · PostgreSQL · Redis · Attu

    npm install
    安装 workspaces 依赖

    配置 .env
    qwen3.6-plus · text-embedding-3-large

    npm run dev
    Turbo 并行启动后端 :3000 · 前端 :5173

    上传文档
    PDF / Word / Excel / HTML / Markdown

    入库流水线
    解析 → 分块 → Embedding → Milvus 混合索引

    对话问答
    LangGraph 检索 · Rerank · 生成 · 引用

    评测面板调参 · 上线前检查

    生产级 RAG 可用
    全程 API 约 100 元

    图例说明:

    • 蓝色节点:Claude Code 负责的部分(方案 → 代码 → 调试)
    • 橙色节点:你在本机执行的构建与运行步骤
    • 绿色终点:系统跑通并可对外提供 RAG 问答

    对应仓库根目录常用命令:

    docker compose up -d # 或 npm run docker:up
    npm install
    # 复制 .env.example 为 .env,填入通义千问 API Key 与模型名
    npm run dev # Turbo 同时启动 backend + frontend
    ┌─────────────────────────────────────────────────────────┐
    前端 (5173)
    Vite + React 19 + TypeScript + TailwindCSS 4
    Zustand 状态管理 · React Query · Lucide 图标
    ├─────────────────────────────────────────────────────────┤
    后端 (3000)
    Node.js + Fastify 5 + TypeScript + LangGraph
    10 节点状态机 · SSE 流式响应 · Zod 配置校验
    ├──────────┬──────────┬──────────┬────────────────────────┤
    Milvus PostgreSQL│ Redis 外部 API
    2.5.x 16 7 阿里百炼
    稠密向量 文档元数据│ 缓存 LLM: qwen-plus
    +BM25稀疏│ Chunk关系 限流 Embedding: v3 1024
    └──────────┴──────────┴──────────┴────────────────────────┘

    核心依赖版本

    组件 版本 说明
    @langchain/langgraph 0.2.x LangGraph 状态机
    @langchain/openai 0.3.x LLM/Embedding 客户端
    @zilliz/milvus2-sdk-node 2.5.x Milvus Node SDK
    fastify 5.x HTTP 框架
    tailwindcss 4.x CSS 框架
    pdf-parse 1.x PDF 解析
    mammoth 1.x DOCX 解析
    xlsx 0.18.x Excel 解析
    cheerio 1.x HTML 解析

    从零搭建的完整过程

    第一阶段:方案文档(施工蓝图)

    在创建任何代码文件之前,第一步是生成 hashed-gliding-metcalfe.md——一份由 Claude Code 输出的生产级 RAG 实现方案。后续所有目录结构、LangGraph 节点、API 设计、技术栈选型,都以这份文档为准;可以说,项目是从 Markdown 规格说明「编译」出来的,而不是边想边写。

    方案文档核心内容:

    • 架构概览:前端四面板 + Fastify 后端 + Milvus/PostgreSQL/Redis + 外部 LLM/Embedding/Rerank
    • 完整目录树packages/backendpackages/frontendpackages/shared 下每个文件的命名与职责
    • LangGraph 流水线:查询改写 → HyDE → 混合检索 → Rerank → 生成 → 自纠正 → 置信度评估
    • 入库流水线:多格式解析 → 语义分块 → 向量化 → Milvus 混合索引

    这份文档保存在仓库根目录,与最终代码结构高度一致,是理解本项目来源的最佳入口。

    第二阶段:基础设施搭建

    2.1 Monorepo 结构

    使用 npm workspaces + Turbo 构建 monorepo,目录结构如下:

    rag-platform/
    ├── package.json # 根配置,定义 workspaces
    ├── turbo.json # Turbo 任务编排
    ├── tsconfig.base.json # 共享 TypeScript 配置
    ├── docker-compose.yml # 基础设施编排
    ├── .env.example # 环境变量模板
    └── packages/
    ├── backend/ # Fastify API 服务
    ├── frontend/ # Vite + React 前端
    └── shared/ # 前后端共享 TypeScript 类型

    关键配置点:

    • package.json 中使用 "workspaces": ["packages/*"] 让 npm 自动管理子包依赖
    • tsconfig.base.json 定义 target: ES2022module: NodeNextstrict: true
    • 子包通过 "extends": "../../tsconfig.base.json" 继承基础配置

    2.2 Docker Compose 基础设施

    services:
    etcd: # Milvus 元数据存储
    minio: # Milvus 对象存储
    milvus: # 向量数据库
    postgres: # 关系型数据库
    redis: # 缓存与限流
    attu: # Milvus Web 管理界面

    2.3 共享类型定义

    packages/shared/src/ 定义了完整的业务类型:

    • document.ts:Document、DocumentCreateInput、DocumentListResponse
    • chunk.ts:Chunk、ChunkCreateInput
    • query.ts:QueryRequest、QueryResponse、Citation、ConfidenceScore
    • config.ts:RAGConfig、DEFAULT_RAG_CONFIG
    • response.ts:SSEEvent、FeedbackInput、EvalMetrics

    第三阶段:后端核心服务

    3.1 文档解析器工厂模式

    // parsers/parser.ts - 解析器接口
    export interface DocumentParser {
    supportedMimeTypes: string[];
    parse(buffer: Buffer): Promise<ParseResult>;
    }
    // parsers/parser.ts - 工厂类
    export class ParserFactory {
    private parsers: DocumentParser[] = [];
    register(parser: DocumentParser) { this.parsers.push(parser); }
    getParser(mimeType: string): DocumentParser {
    return this.parsers.find(p => p.supportedMimeTypes.includes(mimeType))!;
    }
    }

    各解析器实现:

    • PDF:使用 pdf-parse 提取文本,保留页码和元数据
    • DOCX:使用 mammoth 提取纯文本(带结构信息)
    • Excel:使用 xlsx 将每个 sheet 转为 CSV 格式文本
    • HTML:使用 cheerio 清理脚本/样式,再用 turndown 转 Markdown
    • Markdown:直接读取,提取标题作为元数据

    3.2 分块策略路由

    class Chunker {
    async chunk(text, options, documentId) {
    switch (options.strategy) {
    case 'markdown': return this.markdownChunk(...); // 按标题切分
    case 'semantic': return this.semanticChunk(...); // 语义边界
    case 'hierarchical': return this.hierarchicalChunk(...);// 父子结构
    case 'fixed': return this.fixedSizeChunk(...); // 固定大小
    }
    }
    }

    Markdown 分块是最实用的策略:按 # 标题层级切分,保留 headerPath(如 ["第一章", "1.1 节", "1.1.1 小节"])作为元数据,这样检索时能知道 chunk 在文档中的位置。

    3.3 Milvus 向量存储

    // 集合字段设计
    { name: 'id', data_type: DataType.VarChar, is_primary_key: true },
    { name: 'document_id', data_type: DataType.VarChar, is_partition_key: true },
    { name: 'content', data_type: DataType.VarChar, max_length: 65535 },
    { name: 'dense_vector', data_type: DataType.FloatVector, dim: 1024 },
    { name: 'sparse_vector', data_type: DataType.SparseFloatVector },
    { name: 'chunk_index', data_type: DataType.Int64 },
    { name: 'metadata', data_type: DataType.JSON },
    // ... 过滤字段:doc_type, source, author, created_at, section_title

    第四阶段:LangGraph RAG 管道

    设计了完整的 10 节点有向无环图(DAG)带条件分支和自纠正循环:

    START
    classify(查询分类)
    ├─ factual/comparative → decompose(子问题分解)
    │ │
    │ ▼
    │ retrieve(混合检索)
    │ │
    ├─ general/other → rewrite(查询改写)
    │ │
    │ ├─ HyDE enabled → hyde(假设文档)
    │ │ │
    │ └─ HyDE disabled ───────┤
    │ ▼
    │ retrieve(混合检索)
    │ │
    │ ▼
    │ rerank(重排序)
    │ │
    │ ▼
    │ compress(上下文压缩)
    │ │
    │ ▼
    generate(答案生成)
    │ │
    │ ▼
    │ grade(质量评估)
    │ │
    │ ┌─ pass/ambiguous ───► format(格式化)
    │ │
    │ └─ fail + retries ──► rewrite(回退重写)
    │ │
    │ └─ 循环回去
    END

    第五阶段:前端企业级 UI

    • 侧边栏:深色背景(bg-sidebar)、图标+文字、选中高亮、可折叠
    • 对话界面:左侧多会话管理、中间聊天气泡、右侧引用面板、底部输入区
    • 文档管理:4 个统计卡片、搜索/筛选/视图切换、拖拽上传、列表/网格双视图
    • 评估面板:4 个指标卡片(命中率/MRR/忠实度/相关性)、7 天趋势柱状图、系统状态面板
    • 配置面板:Tab 式布局(检索/模型/分块/高级)、开关/滑块/下拉选择

    踩坑记录与核心难点(重点)

    Bug 1:@types/mammoth 包不存在 —— 依赖安装失败

    异常信息

    npm error 404 Not Found - GET https://registry.npmjs.org/@types%2fmammoth

    排查过程

    1. npm install 失败,提示 @types/mammoth 不存在
    2. 检查 npm registry 确认该类型包确实不存在
    3. 查看 mammoth 源码,发现它自带 TypeScript 类型声明

    解决方案:从 package.jsondevDependencies 中删除 "@types/mammoth": "^1.4.4" 这行。

    教训:不是所有包都有对应的 @types/* 包。先查一下包是否自带 .d.ts 文件,没有再装 @types/*


    Bug 2:minhash 包不存在 —— 去重模块依赖缺失

    异常信息

    npm error notarget No matching version found for minhash@^2.0.0

    排查过程

    1. npm 找不到 minhash 的 2.0.0 版本
    2. 搜索 npm registry 发现该包名已被弃用,现在叫 node-minhash 或其他替代

    解决方案:暂时移除 minhash 依赖,去重功能先用简单的内容哈希(xxhash-wasm)替代。MinHash+LSH 作为后续优化项。


    Bug 3:dotenv 在 monorepo 中找不到 .env 文件

    异常信息

    ZodError: [
    { "code": "invalid_type", "expected": "string", "received": "undefined", "path": ["llmApiKey"] },
    { "code": "invalid_type", "expected": "string", "received": "undefined", "path": ["embeddingApiKey"] },
    { "code": "invalid_type", "expected": "string", "received": "undefined", "path": ["databaseUrl"] }
    ]

    排查过程

    1. 后端启动就崩溃,Zod 报所有环境变量 undefined
    2. .env 文件明明存在于 D:\rag-platform\.env
    3. config/schema.ts 中使用了 import 'dotenv/config'
    4. 问题:dotenv 默认在 process.cwd() 下查找 .env,而 process.cwd()packages/backend/
    5. 环境变量命名也不匹配:.env 中用的是 LLM_BASE_URL,但 schema 中定义的字段名是驼峰 llmBaseUrl

    解决方案(两步修复)

    第一步:修复 .env 变量名为大写下划线(与 shell 环境变量命名习惯一致):

    // config/schema.ts
    export const ConfigSchema = z.object({
    LLM_BASE_URL: z.string().default('https://api.openai.com/v1'),
    LLM_API_KEY: z.string(),
    // ... 全部使用大写下划线
    });

    第二步:显式指定 .env 路径:

    import path from 'node:path';
    import { fileURLToPath } from 'node:url';
    import dotenv from 'dotenv';
    const __dirname = path.dirname(fileURLToPath(import.meta.url));
    dotenv.config({ path: path.resolve(__dirname, '../../../../.env') });

    排查技巧:在 getConfig() 中加 console.log(process.env.LLM_API_KEY) 确认环境变量是否被加载。


    Bug 4:LangGraph 节点名与状态字段名冲突

    异常信息

    Error: grade is already being used as a state attribute (a.k.a. a channel), cannot also be used as a node name.

    排查过程

    1. 后端启动崩溃,错误指向 graph.ts 第 48 行
    2. 检查发现 RAGState 中定义了 grade: Annotation()
    3. 同时又用 .addNode('grade', gradeNode) 注册了同名节点
    4. LangGraph 要求节点名和状态字段名不能重复

    解决方案:将节点名从 'grade' 改为 'evaluate',同时更新所有引用该节点的边:

    workflow.addConditionalEdges('evaluate', routeAfterGrade, {
    format: 'format',
    retry: 'rewrite',
    });

    Bug 5:Milvus createIndexparams 参数格式错误

    异常信息

    Error: ErrorCode: UnexpectedError. Reason: there is no vector index on field: [dense_vector], please create index firstly

    排查过程

    1. Milvus 集合创建成功,但 loadCollection 时报错说没有向量索引
    2. 检查 createIndex 调用代码,使用的是 extra_params: JSON.stringify({ nlist: 1024 })
    3. 搜索 Milvus Node SDK 文档和源码,发现新版 SDK 使用 params 参数且期望传 对象
    4. 传 JSON 字符串导致 Go 后端反序列化失败,索引创建静默失败

    解决思路

    1. 先查 Milvus SDK 的 createIndex 方法签名
    2. 发现 params 应该是对象而非字符串
    3. 改为 params: { nlist: 1024 } 后问题解决
    // ❌ 错误写法
    await milvus.createIndex({
    index_type: 'IVF_FLAT',
    metric_type: 'COSINE',
    extra_params: JSON.stringify({ nlist: 1024 }),
    });
    // ✅ 正确写法
    await milvus.createIndex({
    index_type: 'IVF_FLAT',
    metric_type: 'COSINE',
    params: { nlist: 1024 },
    });

    Bug 6:TailwindCSS v4 样式不生效

    异常信息:无报错,但前端页面没有样式,所有 Tailwind 类名都失效。

    排查过程

    1. 页面能打开,但就是纯 HTML 堆砌,没有任何样式
    2. 检查 index.css 中有 @import "tailwindcss"
    3. 检查 vite.config.ts 中只有 [react()] 插件
    4. TailwindCSS v4 的集成方式变了:需要在 Vite 中配置 @tailwindcss/vite 插件

    解决思路

    1. 搜索 TailwindCSS v4 文档,确认需要 @tailwindcss/vite 插件
    2. npm install @tailwindcss/vite
    3. vite.config.ts 中添加插件
    import tailwindcss from '@tailwindcss/vite';
    export default defineConfig({
    plugins: [react(), tailwindcss()],
    });

    Bug 7:文档入库时重复创建记录

    异常表现:上传一个 PDF 文件,文档列表中显示两条相同名称的记录。

    排查过程

    1. 查询 PostgreSQL 发现确实有两条相同 title 的记录
    2. 追踪代码流程:
      • routes/ingest.ts:收到文件 → documentService.create() → 创建记录 → 调用 ingestionService.ingest()
      • services/ingestion.service.ts:解析文档 → this.documentService.create()又创建了一次
    3. 问题:两个地方各自调用了一次 create()

    解决思路

    1. 路由层负责创建记录(返回 documentId 给前端)
    2. 把 documentId 传给 ingestionService.ingest(documentId, ...)
    3. 服务层复用已有 ID,不再创建新记录
    // routes/ingest.ts
    const documentId = await documentService.create({...});
    ingestionService.ingest(documentId, buffer, fileName, mimeType, ...);
    // services/ingestion.service.ts
    async ingest(documentId: string, fileBuffer: Buffer, ...) {
    // 直接使用传入的 documentId,不再创建
    // ...
    }

    Bug 8:向量维度不匹配(3072 vs 1024)

    异常表现

    • 文档上传成功、分片 8 个、Milvus 中有 8 条数据
    • 但检索返回 0 条结果,没有任何错误提示
    • 使用 curl 直接测试后端 API 也返回空

    排查过程

    第一步:确认 Milvus 中有数据

    node -e "
    const { MilvusClient } = require('@zilliz/milvus2-sdk-node');
    const client = new MilvusClient({ address: 'localhost:19530' });
    const count = await client.query({
    collection_name: 'rag_chunks',
    output_fields: ['count(*)'],
    filter: '',
    });
    console.log('Count:', count.data); // 输出: [{"count(*)":"8"}]

    第二步:检查向量维度

    const sample = await client.query({
    collection_name: 'rag_chunks',
    output_fields: ['id', 'dense_vector'],
    filter: '', limit: 1,
    });
    console.log('Vector length:', sample.data[0].dense_vector.length); // 输出: 1024

    第三步:检查 schema 定义

    // vectorstore/schema.ts
    export const DENSE_VECTOR_DIM = 3072; // text-embedding-3-large 的维度

    发现问题:集合创建时用了 3072 维(默认配置,原本打算用 OpenAI 的 text-embedding-3-large),但实际 embedding API 调用的是阿里百炼的 text-embedding-v3,返回 1024 维。虽然 Milvus 接受了插入(可能截断或补零了),但搜索时维度不匹配导致静默返回空结果。

    解决方案:修改 schema 为实际 embedding 维度

    // vectorstore/schema.ts
    export const DENSE_VECTOR_DIM = 1024; // text-embedding-v3 的维度

    同时需要删除旧集合重建(因为 Milvus 不允许修改已存在集合的维度)。


    Bug 9:LangChain SDK 参数不兼容 —— baseUrl vs configuration: { baseURL }

    异常表现

    • 文档上传成功,8 个 chunks 创建成功
    • 日志显示 "Generated 8 embeddings"
    • Milvus 显示 "Indexed 8 chunks"
    • 但检索时返回 0 条文档
    • 更诡异的是 embedding 阶段耗时极长(数十秒),正常应该 1-2 秒

    排查过程

    第一次尝试:检查 embeddings/openai.ts 代码

    // 当时的代码
    embeddings = new OpenAIEmbeddings({
    model: config.EMBEDDING_MODEL,
    apiKey: config.EMBEDDING_API_KEY,
    baseUrl: config.EMBEDDING_BASE_URL, // ← 这行有问题
    dimensions: config.EMBEDDING_DIMENSION,
    });

    第二次尝试:写独立脚本测试两种参数方式

    # 测试 baseUrl(新版语法)
    node -e "
    const { OpenAIEmbeddings } = require('@langchain/openai');
    const embeddings = new OpenAIEmbeddings({
    model: 'text-embedding-v3',
    apiKey: 'sk-...',
    baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
    dimensions: 1024,
    });
    embeddings.embedDocuments(['test']);
    "
    # 结果:卡住不动,超时
    # 测试 configuration.baseURL(旧版语法)
    node -e "
    const { OpenAIEmbeddings } = require('@langchain/openai');
    const embeddings = new OpenAIEmbeddings({
    model: 'text-embedding-v3',
    apiKey: 'sk-...',
    configuration: { baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1' },
    dimensions: 1024,
    });
    embeddings.embedDocuments(['test']);
    "
    # 结果:238ms 完成,返回 2 个 1024 维向量

    根因@langchain/openaiOpenAIEmbeddings 类中,baseUrl 参数不被识别(被忽略),导致它使用默认的 OpenAI API 地址(api.openai.com),而不是阿里百炼的地址。由于 API Key 不匹配 OpenAI,请求要么超时要么返回错误数据。

    解决方案

    embeddings = new OpenAIEmbeddings({
    model: config.EMBEDDING_MODEL,
    apiKey: config.EMBEDDING_API_KEY,
    configuration: { baseURL: config.EMBEDDING_BASE_URL }, // ✅ 正确
    dimensions: config.EMBEDDING_DIMENSION,
    timeout: 30000,
    });
    // ChatOpenAI 同理
    llm = new ChatOpenAI({
    model: config.LLM_MODEL,
    apiKey: config.LLM_API_KEY,
    configuration: { baseURL: config.LLM_BASE_URL }, // ✅ 正确
    temperature: config.LLM_TEMPERATURE,
    maxTokens: config.LLM_MAX_TOKENS,
    });

    Bug 10:Milvus 集合已存在但索引丢失

    异常表现

    Error: ErrorCode: UnexpectedError. Reason: there is no vector index on field: [dense_vector], please create index firstly

    排查过程

    1. 之前的多次重启/崩溃导致 Milvus 中留下了不完整的集合
    2. hasCollection 检查通过(集合存在),但索引可能没建完
    3. 每次重启后端时 ensureCollections() 跳过重建,导致使用没有索引的旧集合

    解决方案:每次重启后端前手动删除 Milvus 集合

    node -e "
    const { MilvusClient } = require('@zilliz/milvus2-sdk-node');
    const client = new MilvusClient({ address: 'localhost:19530' });
    client.dropCollection({ collection_name: 'rag_chunks' });
    client.dropCollection({ collection_name: 'rag_document_summaries' });
    client.closeConnection();
    "

    更好的方案:在 ensureCollections() 中加入索引存在性检查,如果集合存在但索引缺失则重建。


    Bug 11:相似度阈值过高导致全部过滤

    异常表现

    • 文档已索引,Milvus 中有 8 条数据
    • Embedding API 正常工作
    • 手动向 Milvus 发起随机向量搜索能返回结果(score: 0.007-0.01)
    • 但 RAG 管道中检索返回 0 条

    排查过程

    第一步:在 retrieve.node.ts 中加日志

    console.log(`Retrieved ${documents.length} documents for query`);

    输出:Retrieved 0 documents for query: "周文轩是谁?..."

    第二步:直接在 Node.js 中测试 Milvus 搜索

    node -e "
    const { MilvusClient } = require('@zilliz/milvus2-sdk-node');
    const { OpenAIEmbeddings } = require('@langchain/openai');
    // 生成查询向量
    const embeddings = new OpenAIEmbeddings({
    model: 'text-embedding-v3',
    apiKey: 'sk-...',
    configuration: { baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1' },
    dimensions: 1024,
    });
    const [queryVec] = await embeddings.embedDocuments(['周文轩是谁']);
    // 搜索 Milvus
    const milvus = new MilvusClient({ address: 'localhost:19530' });
    const result = await milvus.search({
    collection_name: 'rag_chunks',
    vector: queryVec,
    topk: 5,
    output_fields: ['id', 'content', 'chunk_index'],
    });
    console.log('Results:', result.results.length);
    console.log('Scores:', result.results.map(r => r.score));
    "

    输出:

    Results: 5
    Scores: [0.4522, 0.3395, 0.3032, 0.2815, 0.2654]

    第三步:发现关键差异——Milvus 直接搜索返回 score 范围是 0.26-0.45,但代码中相似度阈值是 0.7

    根因:阿里百炼 text-embedding-v3 的 COSINE 相似度分数天然偏低,在 0.2-0.45 范围,而 OpenAI 的 embedding 分数通常在 0.7 以上。代码中默认阈值 0.7 是参照 OpenAI 设定的,导致所有结果都被过滤。

    解决方案(两步)

    1. 降低默认阈值:similarityThreshold: 0.2
    2. 移除 retrieve.node.ts 中的阈值过滤,让后续的 reranking 和 grading 来处理质量控制
    // retrieve.node.ts - 移除阈值过滤
    // const filtered = results.filter(r => r.score >= threshold); // ← 删除这行
    const documents = results.map(r => ({ ...r })); // 直接返回所有结果

    Bug 12:Fastify multipart Content-Length 不匹配

    异常信息

    {
    "statusCode": 400,
    "code": "FST_ERR_CTP_INVALID_CONTENT_LENGTH",
    "message": "Request body size did not match Content-Length"
    }

    排查过程

    1. curl 直接请求后端 API 正常
    2. 通过前端(Vite dev server 代理)请求返回 400
    3. 问题只在开发模式下出现,生产环境不会有
    4. 原因:Vite 代理在处理 multipart/form-data 时可能修改了 Content-Length 头

    解决方案:生产环境不需要 Vite 代理(nginx 直接代理),开发模式下暂时用 curl 或 Postman 测试文件上传。


    Bug 13:pino-pretty transport 无法解析

    异常信息

    Error: unable to determine transport target for "pino-pretty"

    排查过程

    1. Fastify logger 配置了 transport: { target: 'pino-pretty' }
    2. pino-pretty 没有安装
    3. 在 ESM 模式下,pino 的 transport target 解析方式不同

    解决方案:直接移除 transport 配置,使用 Fastify 默认的 JSON 日志格式

    const app = Fastify({
    logger: {
    level: config.NODE_ENV === 'development' ? 'debug' : 'info',
    // 移除 transport 配置
    },
    });

    Bug 14:后端进程端口冲突

    异常信息

    Error: listen EADDRINUSE: address already in use 0.0.0.0:3000

    排查过程

    1. 新启动的后端进程报错端口占用
    2. 旧的后端进程还在运行(tsx watch 模式不会自动退出)
    3. tsx watch 会监听文件变化自动重启,但有时候会 fork 出多个进程

    解决方案

    # 找到占用 3000 端口的进程 PID
    netstat -ano | grep 3000 | grep LISTENING
    # 强制终止
    taskkill //PID //F

    排查方法论总结

    在整个开发过程中,形成了以下排查策略:

    策略 1:逐层隔离测试

    当 RAG 管道返回 0 条结果时,按以下顺序逐层测试:

    1 层:Milvus 中有数据吗?
    → node 查询 count(*) → 8 条 ✓
    2 层:Milvus 中的向量维度正确吗?
    → 查询 sample dense_vector.length → 1024
    3 层:Milvus 能搜到结果吗?
    → 直接 search → 5 条,score: 0.26-0.45
    4 层:Embedding API 调用的是正确的地址吗?
    → 独立脚本测试 baseUrl vs configuration → baseUrl 超时 ✗
    5 层:代码中的阈值过滤是否合理?
    → 阈值 0.7 > 实际分数 0.45 → 全部过滤 ✗

    策略 2:对比测试

    当怀疑某个参数配置不正确时,写两个独立脚本对比:

    # 方案 A
    node -e "new OpenAIEmbeddings({ baseUrl: ... }).embedDocuments(['test'])"
    # 结果:超时
    # 方案 B
    node -e "new OpenAIEmbeddings({ configuration: { baseURL: ... } }).embedDocuments(['test'])"
    # 结果:238ms 成功

    策略 3:日志驱动

    在每个关键步骤加 console.log

    Parsing document: xxx.pdf 5868 characters
    Created 8 chunks
    Generated 8 embeddings
    Indexed 8 chunks in Milvus
    Ingestion completed in 789ms

    如果某个步骤没有日志输出,问题就在那一步。

    策略 4:直接查日志文件

    当后端在后台运行时,输出被写入临时文件:

    C:\Users\ADMINI~1\AppData\Local\Temp\claude\D--\...\tasks\{task-id}.output

    通过 Read 工具读取完整日志,而不是等后台通知。


    容易出问题的关键检查点

    上线前必查清单

    # 检查项 验证方法
    1 Embedding 维度一致性 Milvus schema 的 dim 值 == 模型实际输出维度
    2 API 地址参数 使用 configuration: { baseURL } 而非 baseUrl
    3 相似度阈值 先用独立脚本测试 embedding 的分数分布
    4 Milvus 索引创建 params 传对象而非 JSON 字符串
    5 环境变量加载 monorepo 中需要指定 .env 的绝对路径
    6 文档入库去重 路由层和服务层不会各自创建数据库记录
    7 TailwindCSS 插件 v4 必须在 vite.config.ts 中注册
    8 后端进程 重启前确保旧进程已退出,避免端口占用
    9 Milvus 集合状态 重启后端时可能需要删除旧集合重建
    10 LangGraph 节点名 不能与 RAGState 中的字段名重复

    开发调试建议

    1. 写独立测试脚本:对每个可能出问题的组件(Embedding、Milvus 搜索、LLM 调用)写一个独立的可运行脚本,不要等到整个系统跑起来才测试
    2. 打印关键指标:在 retrieve.node.ts 中打印检索到的文档数量和前几个 score 值,确认过滤逻辑正确
    3. 使用 Attu 管理界面:Milvus 的 Attu( http://localhost:3001 )可以直观查看集合、数据和索引状态
    4. PostgreSQL 直查:用 psql 或 SQL 客户端直接查询 documentschunks 表,确认数据入库
    5. 后端进程管理tsx 的 watch 模式容易残留进程,建议在开发阶段用 npx tsx src/index.ts(不 watch)代替 npm run dev(watch)

    项目总结

    本项目从零到完整可用的 RAG 平台,共修复了 14 个 Bug,经历了无数次重启和重试。起点是 Claude Code 生成的方案文档 hashed-gliding-metcalfe.md,代码据此逐模块落地;模型侧依赖 通义千问 qwen3.6-plustext-embedding-3-large,全链路 API 花费约 100 元——可作为「先写规格、再 AI 生成代码 + 国内 LLM」完成生产级 RAG 的一次 Token 与成本样本。

    Bug 分类

    类型 数量 典型问题
    SDK 参数不兼容 3 baseUrl vs configuration.baseURLparams vs extra_params
    参数不匹配 3 向量维度 3072 vs 1024、相似度阈值 0.7 vs 0.45
    配置/路径问题 3 .env 找不到、Tailwind 插件未注册、端口占用
    逻辑 Bug 3 重复创建记录、节点名冲突、索引创建静默失败
    环境问题 2 @types/mammoth 不存在、minhash 包被弃用

    最大教训

    这些问题的共同特点是:不会抛明确的错误,而是静默失败——返回 0 条结果、向量全是 0、配置被忽略、请求超时但不报错。

    排查这类问题的核心方法论:

    1. 不要相信系统没报错就是正常的——没有结果本身就是一个错误信号
    2. 逐层隔离,从下往上测——先确认数据库/向量库正常工作,再测服务层,最后测管道层
    3. 写独立测试脚本——不要等到整个系统跑起来才测试单个组件
    4. 打印关键指标——每个步骤都打印输入输出的数量和关键值
    5. 对比验证——用已知的正确方式(如 curl、独立脚本)对比可疑的代码路径

    附录:完整文件清单

    D:\rag-platform\
    ├── .env # 环境变量配置
    ├── .env.example # 环境变量模板
    ├── .gitignore
    ├── docker-compose.yml # 基础设施编排
    ├── package.json # Monorepo 根配置
    ├── tsconfig.base.json # 共享 TypeScript 配置
    ├── turbo.json # Turbo 任务编排
    ├── hashed-gliding-metcalfe.md # 实现方案(代码生成前的施工蓝图)
    ├── PROJECT_JOURNEY.md # 本文档
    ├── packages/backend/
    │ ├── package.json
    │ ├── tsconfig.json
    │ └── src/
    │ ├── index.ts # 后端入口
    │ ├── config/
    │ │ ├── index.ts # 配置导出
    │ │ └── schema.ts # Zod 校验 schema
    │ ├── server/
    │ │ ├── app.ts # Fastify 实例
    │ │ └── routes/
    │ │ ├── config.ts # /api/v1/config
    │ │ ├── documents.ts # /api/v1/documents CRUD
    │ │ ├── eval.ts # /api/v1/eval/*
    │ │ ├── feedback.ts # /api/v1/feedback
    │ │ ├── ingest.ts # /api/v1/documents POST
    │ │ └── query.ts # /api/v1/query POST
    │ ├── services/
    │ │ ├── document.service.ts # 文档 CRUD
    │ │ ├── evaluation.service.ts # 评估服务
    │ │ ├── feedback.service.ts # 反馈服务
    │ │ ├── generation.service.ts # LLM 生成
    │ │ ├── ingestion.service.ts # 文档入库
    │ │ └── retrieval.service.ts # 检索服务 + RRF
    │ ├── graph/
    │ │ ├── state.ts # LangGraph 状态定义
    │ │ ├── graph.ts # 图编译
    │ │ ├── nodes/ # 10 个节点
    │ │ │ ├── classify.node.ts
    │ │ │ ├── compress.node.ts
    │ │ │ ├── decompose.node.ts
    │ │ │ ├── format.node.ts
    │ │ │ ├── generate.node.ts
    │ │ │ ├── grade.node.ts
    │ │ │ ├── hyde.node.ts
    │ │ │ ├── rerank.node.ts
    │ │ │ ├── retrieve.node.ts
    │ │ │ └── rewrite.node.ts
    │ │ └── subgraphs/
    │ │ ├── correction.graph.ts
    │ │ └── retrieval.graph.ts
    │ ├── llm/
    │ │ ├── openai.ts # ChatOpenAI 客户端
    │ │ ├── provider.ts
    │ │ ├── models.ts
    │ │ └── prompts/
    │ │ ├── rag.prompt.ts
    │ │ ├── grade.prompt.ts
    │ │ └── hyde.prompt.ts
    │ ├── embeddings/
    │ │ ├── openai.ts # OpenAIEmbeddings 客户端
    │ │ └── provider.ts
    │ ├── reranker/
    │ │ ├── cohere.ts
    │ │ ├── cross-encoder.ts
    │ │ └── provider.ts
    │ ├── chunking/
    │ │ ├── chunker.ts # 分块策略路由
    │ │ └── strategies/
    │ │ ├── fixed.ts
    │ │ ├── hierarchical.ts
    │ │ ├── markdown.ts
    │ │ └── semantic.ts
    │ ├── parsers/
    │ │ ├── parser.ts # 解析器接口+工厂
    │ │ ├── pdf.parser.ts
    │ │ ├── docx.parser.ts
    │ │ ├── excel.parser.ts
    │ │ ├── html.parser.ts
    │ │ └── markdown.parser.ts
    │ ├── vectorstore/
    │ │ ├── milvus.ts # Milvus 客户端封装
    │ │ ├── collections.ts
    │ │ ├── schema.ts # 集合 schema
    │ │ └── bm25.ts # BM25 稀疏向量
    │ ├── metadata/
    │ │ ├── extractor.ts
    │ │ └── filter.ts
    │ ├── dedup/
    │ │ └── deduplicator.ts
    │ ├── db/
    │ │ ├── postgres.ts # PostgreSQL 客户端
    │ │ └── migrations/
    │ │ └── 001_initial.sql
    │ ├── cache/
    │ │ └── redis.ts
    │ ├── tracing/
    │ │ └── tracer.ts
    │ └── types/
    │ ├── document.ts
    │ ├── chunk.ts
    │ ├── query.ts
    │ └── eval.ts
    ├── packages/frontend/
    │ ├── package.json
    │ ├── tsconfig.json
    │ ├── vite.config.ts
    │ ├── index.html
    │ └── src/
    │ ├── main.tsx
    │ ├── App.tsx
    │ ├── styles/index.css
    │ ├── api/
    │ │ ├── client.ts
    │ │ ├── documents.ts
    │ │ ├── eval.ts
    │ │ └── query.ts
    │ ├── components/
    │ │ ├── chat/
    │ │ │ ├── ChatWindow.tsx
    │ │ │ ├── MessageList.tsx
    │ │ │ ├── MessageBubble.tsx
    │ │ │ ├── QueryInput.tsx
    │ │ │ ├── CitationPanel.tsx
    │ │ │ ├── ConfidenceBadge.tsx
    │ │ │ └── FeedbackButtons.tsx
    │ │ ├── upload/
    │ │ │ ├── DocumentManager.tsx
    │ │ │ ├── DocumentUpload.tsx
    │ │ │ ├── UploadProgress.tsx
    │ │ │ └── DocumentList.tsx
    │ │ ├── eval/
    │ │ │ ├── EvalDashboard.tsx
    │ │ │ ├── MetricsChart.tsx
    │ │ │ └── ComparisonView.tsx
    │ │ └── config/
    │ │ └── ConfigPanel.tsx
    │ ├── hooks/
    │ │ ├── useQuery.ts
    │ │ ├── useUpload.ts
    │ │ └── useSSE.ts
    │ └── store/
    │ ├── chatStore.ts
    │ └── configStore.ts
    └── packages/shared/
    ├── package.json
    ├── tsconfig.json
    └── src/
    ├── index.ts
    └── types/
    ├── config.ts
    ├── chunk.ts
    ├── document.ts
    ├── query.ts
    └── response.ts

    源码下载:rag-platform.zip

    freesky51_weixin

  • 相关阅读:
    人工智能论文GPT-3(2):2020.5 Language Models are Few-Shot Learners;微调;少样本Few-Shot (FS)
    Anconda环境中python默认不是该环境下的python
    SpringBoot整理-Actuator
    leetcode贪心算法:Gas Station
    crp Week1周报
    bug是测不完的,根本测不完
    opencascade 布尔运算笔记
    基于Bert模型的中文语义相似度匹配算法(离线模式)
    IEEE PDF eXpress系统报错:TimesNewRoman PS-BoldMT, ItalicMT, PSM
    python查找与排序算法详解(示意图+代码、看完基础不成问题)
  • 原文地址:https://www.cnblogs.com/freesky51/p/20221337