背景: 基于之前相关技术性积累,实现RAG的工程,GitHub。

mono — AI 知识库 monorepo(样板框架)

框架/检索编排/数据闭环/工作流端到端/可观测与生产化,全链路实测。

结构

mono/
├── apps/
│   ├── api/          # @repo/api —— Hono API 网关(框架核心,集成一律为插件)
│   ├── data/         # @repo/data —— 多仓库→多向量库增量同步(webhook→Stream→diff→嵌入→per-repo collection)
│   ├── ingestion/    # @repo/ingestion —— 摄入服务(生产:chokidar+webhook+轮询 → XADD;消费:切分/嵌入 → Qdrant upsert)
│   └── web/          # @repo/web —— Next 14 + React Flow + shadcn 风格组件
└── packages/
    └── types/        # @repo/types —— 共享 zod schema + 类型(唯一事实源)

常用命令

命令

说明

pnpm install

安装全部 workspace 依赖

pnpm dev

types 构建后并行启动 api(3000) + ingestion(3002)

pnpm dev:data

启动 data 服务(webhook + worker + 对账一体化,3003)

pnpm dev:web

构建 types/api 后启动 web(3001)

pnpm build

types → api → ingestion → data → web

pnpm typecheck / pnpm test / pnpm lint

全仓类型检查 / 测试 / 检查

核心能力

  • 数据写入闭环:文件放入 INGEST_DIR → chokidar 事件 → XADD ingestion:events → Consumer Group 消费 → 句边界切分(512/50)→ DashScope 批量嵌入 → Qdrant upsert(同 doc_hash 幂等)。

  • 多仓库 → 多向量库增量同步:apps/data 把任意数量的 GitHub 仓库同步进 Qdrant——每仓库一个独立 collection,push 时只同步本次提交的 A/M/D/R 文件变化(见下文专节)。

  • 真实 RAG 管线:POST /v1/retrieval/query —— 嵌入 → 远程 Qdrant 多集合检索(knowledgeOfAI 命名向量 + apps/data 同步的 per-repo 未命名向量集合,RAG_SEARCH_COLLECTIONS 声明清单,逐集合查询按 score 合并取 topK)→ qwen-plus 中文合成。

  • 工作流编排(端到端):web 画布编辑(增删节点/连线/条件边)→ 保存(PUT,改图自动升版本)→ 版本历史浏览 → POST /v1/workflows/:id/run 多轮执行(messages 历史重放 + MemorySaver checkpointer)。

  • 摄入三来源:chokidar / POST /webhooks/:source / 定时轮询 hash 比对。

  • 端到端类型安全:@repo/types zod schema → api 路由 → AppType → web hc<AppType>。

  • 外部数据供给:知识库作为数据源对 其他 LLM / Claude Code 开放——纯检索端点(/v1/retrieval/search)、MCP Server(apps/mcp,Claude Code 一条命令接入)、Qdrant/PG 直连,详见 DATA_ACCESS.md。

架构说明(对 plan.md 的实现方式)

总体架构

                      ┌─────────────────────────────────────────────┐
                      │  远程基础设施(凭据集中于根 .env JSON 总账)      │
                      │  PostgreSQL :5433   Redis :6380   Qdrant :6333 │
                      │  DashScope(千问 qwen-plus + text-embedding-v4)│
                      └──────┬──────────────┬──────────────┬──────────┘
                             │              │              │
   ┌─────────────┐    ┌──────▼──────┐  ┌────▼───────┐  ┌───▼───────────┐
   │ apps/web    │    │  apps/api   │  │ ingestion  │  │  Langfuse(可选) │
   │ Next 14     │hc  │ Hono 网关    │  │ chokidar   │  │  OTel SDK     │
   │ React Flow  ├───▶│ 插件体系     │  │ webhook    │  │  (SpanProcessor│
   │ 画布/检索面板 │    │ ┌─────────┐ │  │ 轮询兜底    │  │   + 业务埋点)  │
   └─────────────┘    │ │langgraph│ │  │   │ XADD   │  └───────────────┘
        AppType       │ │llamaindex│ │  │   ▼       │
   (端到端类型安全)   │ │qdrant   │ │  │ Redis Stream
                      │ │drizzle  │ │  │   │ XREADGROUP(Consumer Group)
                      │ │ioredis  │ │  │   ▼
                      │ │observab.│ │  │ 切分(512/50)→嵌入→Qdrant upsert
                      │ └─────────┘ │  └───────────────┘
                      └─────────────┘
                 packages/types —— zod schema 唯一事实源(全链路复用)

技术选型与理由

plan.md 蓝图

实际实现

为什么这样用

Next.js 14

next@14.2.35 精确钉版

蓝图指定;React 18 严格配套(15/16 升 React 19 会破坏 @xyflow/react 兼容面)

React Flow

@xyflow/react v12

官方新包名(reactflow v11 已弃);画布编辑与只读预览共用一套受控组件

shadcn/ui

手工组件(button/card/input/badge)+ CSS 变量 token

不依赖 CLI 的确定性;暗色可读性问题通过「React Flow 样式全部走 CSS 变量」根治

hono/client 类型安全

AppType = 链式累积 Schema 的 typeof app,web 端 hc<AppType>

Hono 的 route() 返回 Schema 是 union(keyof 塌缩),mount 函数 cast 成交叉 + 泛型参数保留累积——详见 CLAUDE.md 的 AppType gotchas

Hono API 网关

自研 PluginRegistry 插件体系(非中间件堆叠)

集成需要生命周期:topo 依赖排序、zod 配置校验、懒连接 init、优雅停机 dispose、就绪状态(/readyz)——中间件模型表达不了这些

LangGraph.js 编排

@langchain/langgraph v1.4:可序列化 WorkflowGraph → 编译成执行图

图存 PG(jsonb),运行时编译执行;router 出边 condition=路由键的约定使图与引擎解耦

LlamaIndexTS 检索+索引

检索用 @llamaindex/openai(嵌入/LLM)+ qdrant client 直连;索引自实现切分器(TS 版无 SentenceSplitter)

弃用 @llamaindex/qdrant(0.1.33 与 core 0.6.22 版本错位);直连 client.query({using: 命名向量}) 精确对齐 ../RAG 的集合结构

Langfuse v5 OTel

LangfuseSpanProcessor(非 Exporter)+ 业务埋点(rag.pipeline/embed/search/synthesize、llm.chat、langgraph.node)

v5 API 已改;埋点用全局 no-op tracer(SDK 未启动也安全),配密钥即上报

drizzle + PG

workflows + workflow_versions 两表,图存 jsonb;PUT 改图自动 version+1 写历史

版本历史是编排可追溯的基础;懒连接(无 PG 时降级 503)

ioredis

三用途:Stream(XADD/XREADGROUP 队列)、PubSub(摄入通知)、缓存(检索响应 setex)

一个客户端覆盖蓝图三种需求;未配置时各自降级

chokidar

v5(ESM-only、无 glob)+ 启动基线扫描 + sha256 内容去重

新版本 API;ignoreInitial 之外的存量文件用基线哈希,避免启动风暴

@repo/types

zod v3 唯一事实源(全仓钉 v3)

zod v4 与 v3 schema 混用运行时崩溃(踩过);schema → zValidator → AppType → hc 一条链

实现状态(五个阶段全部完成)

  1. 框架骨架:插件体系(6 集成懒连接 + 优雅降级)、@repo/types、错误语义(503 未配置 / 502 上游失败)

  2. 检索与编排:真实 RAG 管线(嵌入→Qdrant→qwen-plus)、WorkflowGraph 编译执行、ingestion/web 骨架

  3. 数据写入闭环:文件→Stream→Consumer Group→切分→嵌入→upsert(幂等),检索命中新文档实测

  4. 工作流端到端:画布编辑保存、版本历史、多轮 /run(MemorySaver checkpointer + messages 重放)

  5. 可观测与生产化:OTel 业务埋点、检索缓存(实测 80 倍加速)、PubSub 通知、Notion/Confluence 适配器、Docker 部署化

附加能力:多仓库 → 多向量库增量同步(apps/data,五阶段蓝图之外的扩展,详见下节)。

多仓库 → 多向量库增量同步(apps/data)

apps/data(@repo/data,端口 3003)把任意数量的 GitHub 仓库增量同步进 Qdrant:每个仓库对应一个独立 collection(一对一),仓库 push 时只同步本次提交产生的变化文件(新增/修改/删除/重命名),不做全量重建。与 ingestion(文件目录 → knowledgeOfAI)是两条互补的数据通道。

GitHub push ─▶ [Webhook :3003] ─▶ Redis Stream data-sync:<repo> ─▶ [Sync Worker]
     ▲                            (每仓库一条流,SET NX 租约独占消费)
     │                                │
[Reconcile 对账] ◀── 每 5 分钟比对远端头与 state,落后则补投递(防 webhook 丢失)
                                      │
     Qdrant per-repo collection ◀────┘
     (chinese-buy-us-stock-guide-main 等)
          ▲ git fetch + diff(from=state.sha|空树, to=after)
          │ A/M/D/R 分类 → glob 过滤 → 切块(auto/markdown/code/fixed)
          │ → DashScope 批量嵌入(batch≤10)→ upsert(512KB 分片)
          │ → 全部成功后写 .sync-state/<repo>.state.json(事务化推进)
          └ 幂等点 ID:sha256(repo:file_path:chunk_index);M 差集删除防僵尸向量;
            content_hash 不变跳过;R 内容未变向量搬运免重嵌

核心保证:

  • 配置驱动:仓库 ↔ collection 映射、glob 过滤、切块策略全部集中在 apps/data/sync.config.yaml(env:VAR 注入),新仓库接入零代码改动

  • 状态外置 + 幂等写入:同步进度存 .sync-state/,向量点 ID 由内容位置确定性生成——任何环节重放都不产生脏数据;worker 侧 isAncestor 收敛检查跳过乱序/重放的旧事件

  • 事务化推进:向量库全部写入成功才更新 state,失败停留在旧 sha 整段可重跑;单文件/单批嵌入与写库带指数退避重试,超上限进仓库 DLQ 流(data-sync:dlq:<repo>)

  • 仓库级隔离 + 水平扩展:每仓库一条 Redis Stream 保证严格 FIFO,租约机制让 worker 可水平扩容、崩溃自动接管(XAUTOCLAIM)

接入与运维:

# 新仓库三步:submodule 拉取 → sync.config.yaml 登记 → 全量初始化
git submodule add <repo-url> apps/data/repos/<name>
pnpm --filter @repo/data cli backfill <name>      # 先加 --dry-run 预览

# 运维命令(sync/backfill/cleanup/status/config-check/reconcile/worker/webhook/serve)
pnpm --filter @repo/data cli status               # 各仓库同步状态与漂移
pnpm dev:data                                     # 一体化启动(webhook+worker+对账)

检索接线:同步进 per-repo collection 的数据经 api 的 RAG_SEARCH_COLLECTIONS 清单被 RAG 管线检索(见「核心能力」):knowledgeOfAI@text-embedding-v4,chinese-buy-us-stock-guide-main——带 @ 的是命名向量集合,不带 @ 的是 data 系统的未命名向量集合;单集合未创建仅告警跳过。已接入仓库:chinese-buy-us-stock-guide(美股指南,13 文档 45 块,已实测检索命中)。详见 apps/data/README.md。

注:apps/data 暂未 Docker 化(pnpm dev:data 直跑长驻 Node 进程);repos/ 与 .sync-state/ 已 gitignore,凭据复用 .env(QDRANT_URL / REDIS_URL / OPENAI_API_KEY)。

待改进(已知短板)

项

现状

改进方向

Langfuse 上报

埋点已就绪但缺密钥未实测;OTel SDK 2.x + api 1.9 组合下 span 父子链存疑

提供 LANGFUSE_* 后实测 trace 树完整性,必要时对齐 OTel 版本

SaaS 正文拉取

Notion/Confluence 适配器只做「内容指针规范化」,不拉正文

各源连接器(API token)拉取 + 再嵌入

索引增量

同 doc_hash 全量重写(先删后写);缓存 TTL 有陈旧窗口

块级增量 diff;api 订阅 ingestion:notifications 主动失效缓存(频道已就绪)

checkpointer

MemorySaver 进程内存,重启丢状态

PG checkpointer(drizzle 已就绪)

画布编辑

无撤销/重做、无节点 config 表单;router 条件为字符串约定

命令栈、节点属性面板、条件表达式 DSL

认证/授权

骨架未做

网关层 auth 中间件(插件机制可挂载)

编排与部署

docker compose 一键起已就绪(api/web/ingestion/data + Caddy 反代 + 可选 local-infra);CI 已就绪(.github/workflows/ci.yml:typecheck/test/lint + compose 校验 + 四镜像构建);无 e2e 测试

e2e 冒烟(起栈后打真实端点)

部署(Docker)

服务器部署完整步骤见 deploy/DEPLOY.md(前置条件、逐步命令、构建镜像的三种方式、日常运维与回滚、故障排查表、安全清单)。下面是速览。

一键起:docker compose(推荐)

docker-compose.yml 编排 api / web / ingestion / data 四服务,外加两个可选 profile(proxy 反代、local-infra 容器版基础设施):

cp deploy/api.env.example       deploy/api.env        # 4 份凭据文件(deploy/*.env 已 gitignore)
cp deploy/ingestion.env.example deploy/ingestion.env
cp deploy/data.env.example      deploy/data.env
cp deploy/web.env.example       deploy/web.env

pnpm docker:up                                        # = docker compose --env-file deploy/compose.env up -d --build
pnpm docker:ps                                        # 四个服务(端口默认只绑 127.0.0.1)
# 另两个 profile(需手动带 flag,脚本未封装):
#   docker compose --env-file deploy/compose.env --profile proxy up -d         # + Caddy 同源反代
#   docker compose --env-file deploy/compose.env --profile local-infra up -d   # + 容器版 PG/Redis/Qdrant

⚠️ 所有 compose 命令都必须带 --env-file deploy/compose.env(或走 pnpm docker:*)。
compose 默认读项目根目录的 .env 做插值,而本仓根 .env 是 JSON 凭据总账,会直接报
unexpected character "{" 而无法启动。deploy/compose.env 只放端口/域名/URL 等公开变量。

  • 同源反代(生产推荐):在 deploy/compose.env 设 NEXT_PUBLIC_API_URL=https://<域名>/api 与 SITE_ADDRESS=<域名>,再 pnpm docker:up + 起 proxy profile —— api 无需暴露公网(api 业务路由目前无鉴权,见待改进表)。
    Caddy 路由表(handle 块按书写顺序匹配,顺序不能乱):

路径

去向

说明

/api/auth/*

web:3001

web 自己的 Next route handler(签发/清除会话 cookie)。必须排在 /api/* 之前——否则被后端抢走后登录页会 404

/api/*

api:3000(剥前缀)

浏览器与 web 服务端都经此访问后端

/webhook/github

data:3003

GitHub push 入口(HMAC 校验在应用内)

/webhooks/*

ingestion:3002

默认注释掉,按需放开

其余

web:3001

前端页面

  • 实测:/api/healthz 命中 api、/login 命中 web、/webhook/github 命中 data、漏写 /api 前缀的 /v1/* 落到 web 404、POST /api/auth/login 命中 web 的 handler。

  • 服务端 API 地址分离(API_URL_INTERNAL):容器化后 web 的服务端(route handler / RSC)不能复用 NEXT_PUBLIC_API_URL——直连模式下容器里的 localhost 是 web 自己(ECONNREFUSED),同源模式下要绕公网域名回来(hairpin)。故 apps/web/src/lib/api.ts 在服务端优先读 API_URL_INTERNAL,compose 已固定注入 http://api:3000(见该文件注释)。排查"登录永远 503 auth_unavailable"时先看这里。

  • GitHub webhook 入口:Caddy 已把 /webhook/github 转给 data:3003(每仓库 HMAC secret 校验在应用内);ingestion 的 /webhooks/* 默认不对外,按需在 deploy/Caddyfile 放开。

  • ⚠️ 环境变量填写规则(实测踩过):可选变量必须整行注释掉,不要写成 KEY= 留空。compose 的 env_file 会把空值注入成空字符串,对 z.string().url().optional() 是「存在但非法」→ ingestion / data 直接 Exited(1) 崩溃(api 因插件注册表容错,只降级 disabled)。各 .env.example 已按此规则写好注释。

  • data 前置:容器内不做 git clone(git.ts 只对已存在仓库做 fetch/diff)——须先在宿主机 git clone <url> apps/data/repos/<name>;repos/ 与 .sync-state/ 经 bind mount 持久化,镜像已装 git/openssh-client,并放行 safe.directory(bind mount 属主与容器 root 不一致)。data 的 env_file 是唯一 required: true(其凭据为设计上的硬需求,缺文件时 compose 直接报错而不是崩溃循环)。

  • 构建期内联:NEXT_PUBLIC_API_URL 改动后必须重建 web 镜像(--build),改运行时 env 无效;web 的 CMD 是 node apps/web/server.js(output: "standalone" 产物),不读 /app/.env,其运行时变量经 env_file / environment 注入。web 镜像实测 ~300MB 级:靠 standalone 产物 + @repo/api 放在 web 的 devDependencies(它只是 type-only 导入;错放 dependencies 会让 pnpm deploy 把整棵后端依赖树打进镜像,实测涨到 954MB,见 apps/web/next.config.mjs 注释)。

手工单容器

四个服务均可从仓库根独立构建(多阶段:安装 → workspace 构建 → 提取生产产物;api/ingestion/data 用 pnpm deploy --prod,web 用 Next 的 output: "standalone"):

docker build -f apps/api/Dockerfile       -t repo-api       .
docker build -f apps/ingestion/Dockerfile -t repo-ingestion .
docker build -f apps/data/Dockerfile      -t repo-data      .
docker build -f apps/web/Dockerfile       -t repo-web       .

各容器 .env 内容(docker 化后必须提供)

镜像不含任何凭据。两种注入方式二选一:① docker run --env-file <宿主机 .env>;② 挂载到容器内 /app/.env(api/ingestion 的启动命令已带 --env-file-if-exists=.env 自动加载)。凭据以根 .env(JSON 总账)为唯一事实源,按下方键名拆分到各服务:

api(端口 3000,挂载卷建议:无,无状态)

# ---- 服务 ----
PORT=3000
LOG_LEVEL=info
STRICT_INTEGRATIONS=false        # true = 未配置的集成启动报错

# ---- 远程资源(取自根 .env 总账)----
DATABASE_URL=postgresql://myappuser:****@<host>:5433/myappdb?sslmode=no-verify
REDIS_URL=redis://:****@<host>:6380/0
QDRANT_URL=http://<host>:6333
QDRANT_API_KEY=<token>

# ---- LLM / 嵌入(DashScope OpenAI 兼容)----
OPENAI_API_KEY=<sk-...>
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
RAG_CACHE_TTL=300                # 检索响应缓存秒数,0 禁用(需 REDIS_URL)

# ---- 可观测(可选;配齐才启动 OTel SDK 上报 Langfuse)----
LANGFUSE_PUBLIC_KEY=
LANGFUSE_SECRET_KEY=
LANGFUSE_HOST=https://cloud.langfuse.com

ingestion(端口 3002,挂载卷建议:被监听目录 -v /data/docs:/data/docs)

# ---- 服务 ----
PORT=3002
LOG_LEVEL=info
INGEST_DIR=/data/docs              # 容器内被监听目录(宿主目录经 -v 挂载)
POLL_INTERVAL_MS=60000

# ---- 远程资源 ----
REDIS_URL=redis://:****@<host>:6380/0
QDRANT_URL=http://<host>:6333
QDRANT_API_KEY=<token>

# ---- LLM / 嵌入 ----
OPENAI_API_KEY=<sk-...>
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1

web(端口 3001):运行时只需 AUTH_SECRET(会话 HMAC 签名密钥,<32 字符则登录一律失败);NEXT_PUBLIC_API_URL 是构建期内联的,须在 docker build 时注入:

docker build --build-arg NEXT_PUBLIC_API_URL=http://<api-host>:3000 \
  -f apps/web/Dockerfile -t repo-web .

(或改用 -e NEXT_PUBLIC_API_URL=... 在 docker run 时传入,效果相同。)

运行示例

# 方式一:宿主机 .env 文件注入(推荐,凭据不进镜像)
docker run -d --name repo-api --env-file ./deploy/api.env -p 3000:3000 repo-api

# 方式二:容器内挂载 .env(api/ingestion 启动命令会自动加载)
docker run -d --name repo-api -v ./deploy/api.env:/app/.env -p 3000:3000 repo-api

# ingestion 额外挂载被监听目录
docker run -d --name repo-ingestion --env-file ./deploy/ingestion.env \
  -v /data/docs:/data/docs -p 3002:3002 repo-ingestion

运维对接

  • 健康检查:api GET /healthz(存活,无依赖)与 GET /readyz(就绪:enabled 插件全健康 200,否则 503);data 亦有 /healthz、/readyz(后者上报 redis 可用性)。可直接对接 k8s liveness/readiness probe。compose 的 healthcheck 一律用 /healthz——/readyz 依赖远程 infra,用它做重启判据会误杀。

  • 优雅停机:api/ingestion/data 均监听 SIGTERM 逆序回收资源(redis/pool/OTel flush),k8s terminationGracePeriodSeconds 留 10s+ 即可。

  • 非 Edge 部署:api/ingestion/data 是长驻 Node 进程(@hono/node-server),不可部署到边缘运行时(Vercel Edge/CF Workers);web 是标准 Next.js。

  • MCP 接入(apps/mcp,不进容器):客户端本地 claude mcp add rag-workbench -- node <repo>/apps/mcp/dist/index.js,环境变量 API_URL 指向部署后的 api 地址。

  • 单副本约束:api 的 langgraph checkpointer 是 MemorySaver(进程内存),data 的 repos//.sync-state/ 为本地卷——两者建议 replicas=1;要水平扩展先换 PG checkpointer 与共享卷。