Skip to content
KevinTuCWPublic

About

An enterprise knowledge-base support copilot.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

🛟 helpmate

生产级企业知识库智能客服 —— 真实中文语料 · 混合检索 + 重排 · 带引用问答 · 安全护栏 + 多租户 + 审计 · 多轮会话 · 全链路可观测 · 可复现评测(工具调用为辅)

License: MIT Python FastAPI LangGraph pgvector Langfuse recall@5

「武道AI / AI Engineering Dojo」以阵制胜系列 · 阵 01 · 从 0 到 1 的真实业务系统


helpmate 是一个能「上线」的企业知识库客服系统。主线是知识问答:以 DJI 大疆公开中文文档(用户手册 / FAQ / 售后政策)为知识库,用混合检索 + 重排把问题答准并给出引用。Function Calling 是辅助,只为「查订单、查物流」这类少数实时问题补一条旁路,绝大多数问题走问答主路径。全流程在 Langfuse 可观测,并配一套人工校验 golden set + 评测门禁。它不是 demo —— 语料是真的,检索是混合的,指标是量化的。

helpmate 客服挂件运行界面:答案带 [n] 角标引用、可展开「已参考 4 篇文档」、答完给三条追问推荐

挂件跑在演示宿主页上:答案逐句挂 [n] 引用,底下折叠「已参考 4 篇文档」可展开原文,答完再给三条追问推荐。挂件不抢焦点、不改宿主页样式。

📑 目录

✨ 特性

  • 🗂️ 真实中文语料 —— 17 篇 DJI 文档 → 926 个 chunk,中文为主、夹杂大量英文专有名词(OcuSync、RTH、DJI Care…)。
  • 📄 结构化 ingestion —— HTML 清洗保留标题层级、PDF 表格用 PyMuPDF 抽取并转 Markdown、按章节切块并附元数据。
  • 🔍 混合检索 —— dense(Qwen3-Embedding)+ sparse(Postgres 全文 + jieba 分词)经 RRF 融合,再由 Qwen3-Reranker 重排。中文语义靠向量,型号与参数靠全文,各司其职。
  • 📌 带引用的回答 —— 每条知识库回答都标注来源 [n],无上下文即拒答。
  • 🧰 Function Calling(辅助) —— 订单/物流类少数实时问题自动旁路到 query_order / query_logistics 工具,其余走知识问答主路径。判意图交给小模型(Qwen3-8B),与生成模型分开配置。
  • 🔭 全链路 Trace —— 每次请求在 Langfuse 记录 route / retrieve / rerank / tool / generate,含模型、token、延迟。
  • 📊 可复现评测 —— 50 条人工校验 golden set(+3 条跨租户负例)+ recall@k / MRR / nDCG / 工具路由 / 租户隔离 / 引用指标 + 贴基线阈值;golden 支持内容锚点,重灌库不失效。CI 硬门禁是单测(pytest),指标门禁跑本地/夜跑。
  • 🛡️ 安全护栏 —— 输入拦注入 / 越狱 / 越权诱导,输出脱敏 + 违规内容拦截;纯规则、零额外延迟。
  • 🧾 审计留痕 —— 每次 /chat 落一条不可变审计(租户 / 会话 / 决策 / 护栏结果 + 答案哈希);问题侧先脱敏再入库,可回溯而不留明文 PII。
  • 🏢 身份与授权 —— 调用方身份来自 API Key(X-API-Key)而非请求体:检索按 tenant_id 过滤,订单/物流按 customer_id 做行级归属校验,报一个陌生单号只会得到「未找到」。未配 API_KEYS 时为本地 dev 身份,克隆即跑。
  • 💬 多轮会话 —— 会话历史改写指代(「它 / 这款」),改写只喂检索、不多花一次思考调用。
  • 🧹 语料清洗 + 在线采样 —— 摄取阶段剥离导航/页脚 boilerplate;按比例采样线上流量回流待评。

🏗️ 架构

用户问题 ─▶ 输入护栏 ─▶ [多轮改写] ─▶ route(Qwen3-8B 判意图,小模型)
              ├─ 知识类(主)─▶ retrieve 混合检索(按 tenant 过滤)─▶ rerank(Qwen3)─┐
              │      dense pgvector(HNSW) + sparse tsvector(GIN) → RRF 融合
              └─ 订单/物流(辅)─▶ act:query_order / query_logistics ─┤
              └─ 插件委派(可选)─▶ delegate:外部系统逐字返回 ───────┘
                                                                       ▼
                    generate(GLM-4.7)─▶ 输出护栏 ─▶ 带[n]引用的答案 ─▶ 审计留痕 + 在线采样
  • 模型分工:判意图是二选一的分类,不是生成 —— 交给小模型(Qwen3-8B)比让 GLM-4.7 兼职划算,路由 p50 从 ~13s 降到 ~3s 而 golden 集路由准确率仍是 1.00。
  • 摄取路径:loaders → clean → chunking → pipeline → Qwen3 embed → Postgres
  • 应答路径:护栏(in) → [多轮改写] → route → (act | retrieve → rerank) → generate → 护栏(out) → 审计,由 LangGraph 编排,Langfuse 全程埋点。
  • 插件委派:可选插件可独占一轮回复并绕过 generate。第一个实现是 Return Saver;未配置时图形状与工具 schema 都保持原样。

详细系统设计见 docs/architecture.md,产品需求见 docs/prd.md。

🧱 技术栈

关注点 选型
API / 编排 FastAPI · LangGraph
生成模型 GLM-4.7(z.ai,OpenAI 兼容)
路由模型 Qwen3-8B(SiliconFlow,thinking 关闭)—— 判意图专用
向量嵌入 Qwen3-Embedding-8B(1024 维,SiliconFlow)
重排 Qwen3-Reranker-8B(SiliconFlow)
存储 / 检索 Postgres 16 + pgvector(HNSW)+ tsvector(GIN)
摄取 BeautifulSoup(HTML)· PyMuPDF(PDF + 表格)· 结构感知切块
可观测 Langfuse(v4)
评测 自建指标 + RAGAS(可选)

所有 provider 均为 OpenAI 兼容,可通过 .env 平滑切换。

🚀 快速开始

前置:Python 3.12 · Postgres 16 + pgvector · GLM(z.ai) / SiliconFlow / Langfuse 三个 key。

# 1. 建库并应用 schema(含 governance/ops 表;维度需与 EMBED_DIM 一致)
psql "$DATABASE_URL" -v dim=1024 -f db/schema.sql
psql "$DATABASE_URL" -f db/seed.sql
# 已有旧库、不想重灌语料?改用非破坏迁移:
# psql "$DATABASE_URL" -f db/migrations/001_governance_ops.sql
# psql "$DATABASE_URL" -f db/migrations/002_order_ownership.sql   # 订单归属列
# psql "$DATABASE_URL" -f db/migrations/003_cjk_fts.sql           # 中文全文检索
# python -m scripts.backfill_tsv     # 003 之后必跑:重算 content_tsv(纯本地,无 API 开销)

# 2. 配置密钥
cp .env.example .env        # 填入 GLM / SiliconFlow / Langfuse key

# 3. 安装并构建知识库
pip install -e ".[dev]"
python scripts/fetch_corpus.py         # 抓取 DJI 语料(corpus/sources.tsv)
python scripts/ingest_corpus.py        # 摄取 → chunks
python scripts/backfill_embeddings.py  # Qwen3 回填向量 + 建 HNSW 索引

# 4. 启动服务
make run         # 等价于 uvicorn helpmate.app:app --reload --port 8000

# (可选)跑测试 —— CI 的硬门禁
make test        # 等价于 pytest -q

打开 http://localhost:8000,问一个知识库问题,或者「我的订单 A1001 到哪了?」。

本地 Return Saver 演示可通过 URL 选择种子客户,例如 http://localhost:8000/?customer=C-001。支持 C-001 到 C-010;切换客户参数会自动开启新会话。该参数仅在 localhost / 127.0.0.1 生效。

💬 使用示例

# dev 模式(未配 API_KEYS):身份 = DEFAULT_TENANT / DEFAULT_CUSTOMER
curl -X POST localhost:8000/chat -H 'Content-Type: application/json' \
  -d '{"question":"DJI Care 随心换进水了能保修吗?"}'

# 配了 API_KEYS 之后,身份走凭证(请求体不再接受 tenant_id)
curl -X POST localhost:8000/chat -H 'X-API-Key: sk-dji-alice' \
  -H 'Content-Type: application/json' -d '{"question":"我的订单 A1001 到哪了?"}'
// 知识类 → 混合检索,带引用
{ "answer": "DJI Care 随心换支持进水保修:保障场景明确包含意外进水 [3]…",
  "tool_call": null }

// 规格类 → 命中中文 PDF 手册的规格表
{ "answer": "DJI Mini 4 Pro 的最大起飞重量为 <249 g [1][2]。" }

// 订单类 → Function Calling(仅限调用方自己的订单)
{ "answer": "订单 A1001 目前运输中,预计送达 2026-08-06 [1]。",
  "tool_call": { "name": "query_logistics", "args": { "order_id": "A1001" } } }

// 别人的订单 → 归属校验在 SQL 里拦下,与「不存在」返回同一句,无法枚举
{ "answer": "没有查到订单 A1002 的信息。",
  "tool_call": { "name": "query_order", "args": { "order_id": "A1002" } } }

🖥️ 客服挂件接口

/chat 之外还有一条流式通道和三个推荐问题接口,供 web/widget/ 的客服挂件消费。/chat 行为不变,两条路径共用同一张编译好的 LangGraph 图。

POST /chat/stream(SSE)

入参与 /chat 相同(question + 可选 session_id)。事件序列:

event data 说明
stage {"stage":"route"|"retrieve"|"retrieved"|"act"|"delegate"|"generate"} 节点开工前推送,是真实进度而非定时动画;retrieved 额外带 count
token {"text":"…"} 答案增量。按句子边界成块吐出,不是逐字符 —— 见下
offer {"plugin":"return_saver","offer":{…},"escape_hatch":"…"} 插件报价卡;按钮走 /plugin/{plugin}/{action} 代理
replace {"text":"…"} 输出护栏终检判定拦截,客户端须把整条消息替换为此文案
done {"hits":[…],"tool_call":…,"guard":…} hits 只含 {n,title,section,url},不含正文
error {"message":"stream failed"} 上游失败;已吐出的文字保留

为什么是逐句而不是逐字:check_output 要看到完整答案才能判定,而流式没有「完整答案」直到结束。所以文本先缓冲到句子边界、脱敏后放行,结束时再对全文终检。终检来得太晚,已经显示的内容收不回去,于是拦截表现为「替换」(replace)而不是「阻止」。

为什么审计不会漏:整轮对话跑在一个无 yield 的工作函数里,finally 里统一写审计、落多轮、采样 —— 正常结束、护栏拦截、上游失败、客户端中途断开,四条出口都会经过它。

推荐问题

curl localhost:8000/suggest/hot                              # 开场热门,一次 SQL
curl -G localhost:8000/suggest/match --data-urlencode 'q=限飞' # 输入联想,一次 SQL
curl -X POST localhost:8000/suggest/followups \
  -H 'Content-Type: application/json' \
  -d '{"question":"限飞区怎么解禁","answer":"需要提交申请。","hit_titles":["限飞政策"]}'

前两个读同一个问题库:手写种子 + 本租户 audit_log 里的历史提问(写入时已脱敏),用 ILIKE 子串匹配。没有走全文检索,因为 content_tsv 用的是 to_tsvector('simple', …),Postgres 默认分词器不切中文 —— 整句会塌成一个 token,查不出东西。followups 走小模型(Qwen3-8B)一次调用,在答案渲染完之后异步请求,不占等待时间。

三个接口都是增强功能:任何一个失败都静默返回空列表,绝不影响对话。

嵌入

<script type="module" src="/widget/index.js" data-api="/" data-api-key="sk-dji-alice"></script>

data-api-key 只适用于本地 demo —— 密钥写在页面上等于公开。生产应由服务端下发短期 token,本仓不实现。未配 API_KEYS 时走 dev 模式,clone && run 即可。

会话语义:session_id 存 localStorage,刷新后上下文仍在(后端记着 session_turns,所以「那它续航呢」依然能指代);但对话内容不落浏览器存储,刷新后面板是空的。挂件顶栏的「⋯」换新 session_id。

对话轮次插件

helpmate 可以把某一轮对话整段交给外部系统处理,回复逐字返回、绕过 generate 节点。第一个实现是 Return Saver(退货挽留)。

为什么不是“再注册一个工具”:现有工具产出的是 context,再由 generate 用 RAG 提示词改写。对自带护栏的外部系统,改写会让输出扫描和签名凭证绑定到 另一段文本或金额。

三条保证:

  1. 未配置 RETURN_SAVER_URL 时,不加载插件 schema,图也不出现 delegate 节点。
  2. 外部系统超时、拒连、5xx 时,/chat 与 /chat/stream 落回 RAG,不返回 5xx。
  3. 除 src/helpmate/plugins/return_saver.py 外,helpmate 只认识通用概念:plugin_sessions 与 /plugin/{plugin}/{action}。

启用 Return Saver

先启动 Return Saver。默认端口是 8777:

cd /Users/miaomiao/ai_projects/return-saver
make install
RS_STORE_BACKEND=postgres make run

首次使用 Helpmate 插件桥接时执行迁移:

psql "$DATABASE_URL" -f db/migrations/004_plugin_sessions.sql

单客户本地演示不需要在页面暴露 API key。Helpmate 使用 C-001 作为默认客户:

cd /Users/miaomiao/ai_projects/helpmate

RETURN_SAVER_URL=http://127.0.0.1:8777 \
RETURN_SAVER_API_KEY=rs-local-demo \
RETURN_SAVER_TIMEOUT_S=60 \
DEFAULT_TENANT=public \
DEFAULT_CUSTOMER=C-001 \
make run

RETURN_SAVER_API_KEY 必须非空,否则 Helpmate 不加载插件;Return Saver 本地 dev 模式不会校验该占位值。真实 GLM 回复可能超过默认 20 秒,因此演示环境使用 60 秒。 本地服务调用会忽略桌面 HTTP 代理,避免 localhost 被代理错误返回 503。

需要在同一个页面切换 10 个种子客户时,为 Helpmate 配置演示身份:

API_KEYS='{"sim-c001":"public:C-001","sim-c002":"public:C-002","sim-c003":"public:C-003","sim-c004":"public:C-004","sim-c005":"public:C-005","sim-c006":"public:C-006","sim-c007":"public:C-007","sim-c008":"public:C-008","sim-c009":"public:C-009","sim-c010":"public:C-010"}' \
RETURN_SAVER_URL=http://127.0.0.1:8777 \
RETURN_SAVER_API_KEY=rs-local-demo \
RETURN_SAVER_TIMEOUT_S=60 \
make run

然后打开 http://localhost:8000/?customer=C-001。参数支持 C-001 到 C-010,切换客户时挂件会自动创建新 session;无参数的 localhost 默认使用 C-001。该便利入口只在 localhost / 127.0.0.1 生效,生产环境不会根据 URL 生成凭证。

输入明确的英文或中文售后诉求会自动唤起 Return Saver。建议首轮同时给出订单号, 例如 The tee from ORD-1001 is too tight. Can you arrange a free size exchange?, 系统可在同一轮完成订单确认并返回 offer 卡。点击 Accept 才会调用 /plugin/return_saver/accept 执行签名方案;点击 I still want to return it 会立即进入标准退货。仅输入 I accept 不会执行 offer。

生产环境使用 RETURN_SAVER_API_KEYS={"tenant":"key"} 按租户配置服务密钥。 映射非空时不会回退到共享 key,避免未列出的租户静默借用其他租户凭证。

📊 评测

golden set 驱动的可复现评测闭环,一条命令出报告与门禁:

python eval/run_eval.py     # → eval/report.md

最新基线(53 条 = 50 条问答 + 3 条跨租户负例,生成 glm-4.7 / 路由 Qwen3-8B,k=5):

指标 值 阈值 结果
recall@5 0.93 0.88 ✅
tool_routing 1.00 0.95 ✅
tenant_isolation 1.00 1.00 ✅
MRR 0.80 — —
nDCG@5 0.83 — —

recall@5 从 0.91 升到 0.93,是修好中文全文检索换来的:content_tsv 原本是 to_tsvector('simple', content), 而 Postgres 默认解析器按数据库 ctype 判断字符类别 —— macOS 上 CJK 被判成 blank 直接丢弃(tsvector 为空), glibc 上则整段塌成一个 token。两种情况下中文查询都是 0 命中,所谓「混合检索」对中文实际只有 dense 一条腿。 现在改成 jieba 分词 + 非 ASCII 词映射为 ASCII 代词,「限飞」的全文命中从 0 变成 38 条。

阈值贴着基线留 ~3 个点余量:0.70 的旧阈值退化 20 个点仍会 PASS,那不是门禁。 tool_routing 换小模型后重跑仍是 53/53:小模型不加约束会在知识类问题上编一个占位单号误调工具(0.98), 路由系统提示写死「没有真实单号就不调工具」后回到 1.00,顺带把路由 p50 又压掉 1 秒多。 tenant_isolation 是新增的跨租户负例(同一问题以外部租户身份检索必须零命中)。 单测(make test)是 CI 硬门禁;make gate 这套指标需要真库 + 真 key,跑在本地/夜跑,不在 CI 内。

生成类指标(引用正确率 + RAGAS faithfulness/answer_relevancy)由 eval_generate=True 开启(judge=GLM、embeddings=Qwen3)。golden set 见 eval/golden.jsonl。

🔭 可观测

每次 /chat 都是 Langfuse 里的一条 chat-response trace,观测树类型清晰:

  • generation —— route-select-tool(Qwen3-8B)/ generate-answer(GLM-4.7),自动记模型 + token,两段延迟可分开看
  • embedding —— Qwen3 向量化
  • retriever —— retrieve-context(混合检索)
  • span —— rerank-candidates
  • tool —— tool:query_logistics / tool:query_order

trace 带 session_id、tags、environment,并对 PII/密钥递归脱敏。

📁 项目结构

helpmate/
├── README.md
├── pyproject.toml
├── docker-compose.yml
├── .env.example
├── corpus/                    # DJI 语料来源清单 + manifest(raw 抓取物 gitignore)
│   ├── sources.tsv
│   └── manifest.jsonl
├── .github/workflows/ci.yml  # CI:pytest 硬门禁
├── Makefile                  # make run / test / gate / ci
├── db/
│   ├── schema.sql             # documents / chunks(+tenant) / orders / shipments / audit_log / session_turns / online_eval
│   ├── migrations/            # 001_governance_ops · 002_order_ownership(非破坏迁移)
│   └── seed.sql
├── docs/
│   ├── prd.md                 # 产品需求
│   └── architecture.md        # 系统设计
├── scripts/
│   ├── fetch_corpus.py        # 抓取语料
│   ├── ingest_corpus.py       # 批量摄取
│   ├── backfill_embeddings.py # 回填向量 + 建 HNSW
│   └── backfill_tsv.py        # 重算 content_tsv(改分词规则后重跑,纯本地零 API 开销)
├── src/helpmate/
│   ├── app.py                 # FastAPI:/ingest /chat /chat/stream /suggest/*
│   ├── suggest.py             # 推荐问题:热门(SQL) · 联想(SQL) · 追问(小模型)
│   ├── config.py              # pydantic-settings 配置
│   ├── obs.py                 # Langfuse 初始化 + 脱敏
│   ├── providers.py           # LLM 客户端:生成(GLM) + 路由(Qwen3-8B)(langfuse.openai drop-in)
│   ├── tools.py               # Function Calling 工具 + 分发
│   ├── graph.py               # LangGraph 应答流
│   ├── auth.py                # API Key → Principal(tenant, customer)
│   ├── db.py                  # 检索(租户过滤) / 订单(归属校验) / 审计 / 会话 / 采样
│   ├── ops.py                 # 确定性在线采样门
│   ├── security/
│   │   └── guardrails.py      # 输入/输出护栏(注入·越狱·越权·脱敏·违规)
│   ├── session/
│   │   └── memory.py          # 多轮指代消解 · 历史改写检索查询
│   ├── ingest/
│   │   ├── loaders.py         # HTML 清洗 · PDF+表格抽取 · 表格转 MD
│   │   ├── clean.py           # 语料 boilerplate 清洗
│   │   ├── chunking.py        # 分节 + 元数据分块(含 tenant_id)
│   │   └── pipeline.py        # 摄取编排(load → clean → chunk → write)
│   └── retrieve/
│       ├── embed.py           # Qwen3 嵌入客户端
│       ├── fuse.py            # RRF 融合
│       ├── rerank.py          # Qwen3 重排
│       ├── segment.py         # 中文分词 → ASCII 代词(Postgres 解析器不认中文)
│       ├── hybrid.py          # dense+FTS→RRF→rerank 编排
│       └── context.py         # 带引用的上下文拼装
├── eval/
│   ├── golden.jsonl           # 50 条人工校验评测集 + 3 条跨租户负例
│   ├── relink_golden.py       # chunk_id → 内容锚点(重灌库不失效)
│   ├── metrics.py             # recall@k / MRR / nDCG / 工具 / 引用(TDD)
│   ├── run_eval.py            # 评测 runner + 报告 + 门禁
│   ├── ragas_eval.py          # RAGAS 生成质量指标
│   └── report.md              # 最新评测报告
├── web/
│   ├── index.html             # 演示宿主页(假官网),一行 script 引入挂件
│   └── widget/                # 客服挂件(零构建原生 ES Module)
│       ├── index.js           # 入口:Shadow DOM 装配 + 自动弹窗策略
│       ├── api.js             # 传输:SSE 解析 · session · 三个推荐接口
│       ├── ui.js              # 渲染:气泡 · chips · 联想 · 引用折叠
│       └── style.css          # 挂件样式(注入 Shadow Root)
└── tests/                     # pytest 单元测试

🧩 配置

.env(见 .env.example)关键项:

变量 说明
DATABASE_URL Postgres 连接串
LLM_PROVIDER / LLM_MODEL 生成模型:glm / glm-4.7(z.ai,base URL 内置默认)
GLM_API_KEY GLM(z.ai) 密钥
ROUTER_PROVIDER / ROUTER_MODEL 路由模型:siliconflow / Qwen/Qwen3-8B;设 ROUTER_PROVIDER=llm 则退回用 LLM_MODEL 判意图
EMBED_MODEL / EMBED_DIM Qwen/Qwen3-Embedding-8B / 1024
RERANK_MODEL Qwen/Qwen3-Reranker-8B
SILICONFLOW_API_KEY SiliconFlow 密钥
LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_BASE_URL Langfuse
TOP_K / retrieve_candidates 最终/候选检索条数
eval_recall_k / eval_generate / eval_thresholds 评测参数
API_KEYS JSON:API Key → "tenant" 或 "tenant:customer";留空 = dev 模式
default_tenant / default_customer dev 模式下的租户 / 订单归属身份
guardrails_enabled 是否启用输入输出护栏
llm_timeout_s / llm_max_retries LLM 单次超时 / 重试次数
ingest_max_chunks 单次 /ingest 回填向量的 chunk 上限
session_history_turns / online_sample_rate 多轮改写载入的历史轮数 / 在线采样比例(%)

🗺️ 路线图

  • ✅ 阶段① 真实 DJI 中文语料 + 结构化 ingestion(含 boilerplate 清洗)
  • ✅ 阶段② Qwen3 嵌入回填 + HNSW + dense/FTS/RRF + Qwen3 重排
  • ✅ 阶段③ 50 条 golden set + 指标 + 报告门禁(recall@5=0.93)+ CI(pytest 硬门禁)
  • ✅ 阶段④ Langfuse v4 全链路 trace
  • ✅ 阶段⑤ 治理层:输入/输出护栏 · 审计留痕(问题侧脱敏) · 多租户过滤 · API Key 身份 + 订单行级归属校验
  • ✅ 阶段⑥ 运营层:多轮会话 + 指代消解 · 在线采样回流
  • ⬜ 专题:检索父子块 + 元数据过滤(拉起 manual recall)
  • ⬜ 专题:延迟分级路由 + 流式(p50 38s → 面客可接受)—— 判意图已下放小模型(route p50 13s → 3s),生成侧流式待做
  • ⬜ 生成类指标默认开启(需更快的 judge 模型)
  • ⬜ 真多模态(图像 OCR / 视频转写)· 完整在线 A/B —— 阵 04

📄 许可证

MIT © helpmate

About

An enterprise knowledge-base support copilot.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages