返回开发日志

2026-07-03

DeepSeek 聊天助手接入与心理量表 RAG 推荐库

本次更新将 ChatBox 接入 DeepSeek 后端 API,并基于 EQAI 心理量表总览 CSV 建立本地 RAG 检索库,用于根据用户意图推荐合适量表。

---

date: 2026-07-03

author: Tony

---

DeepSeek 聊天助手接入与心理量表 RAG 推荐库

本次更新概览

今天的主要目标是把原本依赖前端关键词匹配的 ChatBox,升级为一个后端驱动的 LLM 聊天助手,并让它可以基于完整心理量表总览表推荐合适量表。

本次更新完成了三层能力:

  • 前端 ChatBox 将用户自由输入发送到后端 /api/chat
  • 后端通过 OpenAI SDK 连接 DeepSeek
  • 后端从 EQAI心理量表总览表.csv 构建本地 RAG 检索库,并把检索结果注入 DeepSeek prompt,同时返回可点击推荐卡片

这样用户不需要输入精确量表名称,也可以用自然语言描述自己的需求,例如“最近焦虑压力很大”“孩子注意力不集中”“这里有什么量表”,聊天助手会先检索量表库,再结合 LLM 生成解释,并在回复下方展示可点击的推荐量表卡片。

DeepSeek LLM 接入

新增了服务端 DeepSeek helper:

  • src/lib/deepseek.ts

它使用 OpenAI SDK,但配置 DeepSeek 的 OpenAI-compatible endpoint:

baseURL: 'https://api.deepseek.com'

API Key 从服务端环境变量读取:

process.env.DEEPSEEK_API_KEY

前端不会直接拿到 key。ChatBox 只请求项目自己的后端路由:

POST /api/chat/

后端路由文件是:

  • src/app/api/chat/route.ts

这个路由接收前端传来的 conversation messages,调用 DeepSeek,并返回统一结构:

{
  reply: string,
  recommendedCards: Array<{
    title: string,
    subtitle?: string,
    category?: string,
    href?: string,
    code?: string
  }>
}

ChatBox 前端改动

主要改动在:

  • src/components/ChatBox.tsx

原来的 ChatBox 已经有一套 UI、按钮选项、浮动窗口和内嵌窗口样式。本次没有重做 UI,而是在自由输入消息时接入后端。

当前流程是:

  • 用户输入问题
  • 前端把当前 conversation messages 发送到 /api/chat/
  • 后端返回 reply
  • 如果有 recommendedCards,前端在助手回复下方渲染卡片按钮
  • 点击卡片时根据当前 locale 补上 /en/zh 前缀并跳转

这样原有的聊天风格、按钮样式、浮动入口和首页 inline chat 都保留,同时支持 LLM 回复和结构化推荐卡片。

为什么需要 RAG

仅靠 LLM 自己回答存在两个问题:

  • LLM 可能不知道项目里实际有哪些量表
  • LLM 即使说出了量表名称,也不一定稳定返回前端可点击的结构化卡片

因此本次加入了本地 RAG 检索层:

  • 用户问题先进入量表库检索
  • 检索出的 Top-K 量表作为上下文传给 DeepSeek
  • 同一批检索结果也会被转成 recommendedCards

这样 LLM 的自然语言回答和前端卡片来自同一个数据源,减少“文字推荐了一个量表,但下方没有卡片”或“卡片和回复不一致”的情况。

心理量表 CSV 数据源

新增数据文件:

  • EQAI心理量表总览表.csv

这个文件位于项目根目录。CSV 前两行是说明信息,第三行是表头:

编号,量表名称,题目数,量表描述,示例题目

真实量表数据从第四行开始。

当前解析结果是:

  • 212 个量表条目
  • 包含总量表、复杂分量表、简单分量表
  • 每条记录包含编号、量表名称、题目数、描述、示例题目

RAG 检索模块

新增文件:

  • src/lib/scaleRag.ts

它负责把 CSV 转成可检索的本地数据库。

核心结构是:

type ScaleRagEntry = {
  id: string
  name: string
  itemCount: string
  description: string
  exampleItem: string
  typeLabel?: string
}

解析逻辑:

  • 使用 fs.readFileSync 在服务端读取根目录 CSV
  • 使用内置 CSV parser 处理逗号、换行、双引号转义
  • 跳过前两行说明和第三行表头
  • 从量表名称开头的 【总量表】【简单分量表 S1】 等内容中提取类型标签
  • 结果缓存在内存中,避免每次请求重复解析

检索原理

当前 RAG 不是外部向量数据库,而是本地轻量检索。这样部署简单,不需要额外服务,也不会把量表数据发给第三方向量库。

检索过程分成五步:

  • 标准化用户问题:统一小写,移除常见中英文标点,压缩空格
  • 抽取核心检索词:移除“有没有”“我想找”“相关的”“推荐”“量表”“评估”“测评”等宽泛表达,保留真正表达需求的词,例如“时间管理”“情绪状态”“个人能力”
  • 意图扩展:把“焦虑”“压力”“注意力”“学习”“情绪”等常见表达扩展成相关检索词
  • 条目打分:根据量表名称、描述、示例题目、类型标签进行加权
  • 返回 Top-K:取分数最高的量表作为 RAG context 和推荐卡片

当前权重设计:

  • 量表名称命中权重最高
  • 量表描述命中次之
  • 示例题目命中作为辅助信号
  • 总量表在已有命中的前提下有轻微加分
  • “相关”“量表”“推荐”等泛词不再触发宽泛结果;它们只作为用户表达方式的一部分被过滤
  • 只有当用户没有提供明确主题词,并且是在问“有什么量表”“有哪些量表”“全部量表”时,才返回总量表目录入口

这次调整解决了一个推荐一致性问题:用户输入“时间管理”时可以命中时间管理相关量表,但输入“时间管理相关的量表”时,过去会因为包含“量表”“相关”这类宽泛词而偏向更泛的分类。现在检索会先把这类词降权或剔除,使两种表达都集中到“时间管理”这个核心主题上。

示例:

用户:焦虑 压力
命中:
- EQAI抑郁-焦虑-压力量表
- EQAI抑郁-焦虑-压力量表—压力
- EQAI抑郁-焦虑-压力量表—焦虑
用户:注意力 分心
命中:
- EQAI注意缺陷多动问卷—注意分配与易分心
- EQAI注意缺陷多动问卷
- EQAI注意缺陷多动问卷—注意缺陷
用户:这里有什么量表
命中:
- EQAI多维智慧与智力问卷
- EQAI学术能力问卷
- EQAI社会能力问卷
用户:时间管理
命中:
- EQAI学术能力问卷—时间管理
- EQAI多维智慧与智力问卷—目标感与持续行动力
- EQAI学术能力问卷—资源管理策略
用户:时间管理相关的量表
命中:
- EQAI学术能力问卷—时间管理
- EQAI多维智慧与智力问卷—目标感与持续行动力
- EQAI学术能力问卷—资源管理策略

RAG 与 DeepSeek 的协作方式

/api/chat 的主流程是:

  • 取最新一条用户消息
  • 调用 searchScaleRagEntries
  • 调用 formatScaleRagContext
  • 把检索到的量表上下文传入 getDeepSeekChatReply
  • DeepSeek 基于这些候选量表生成自然语言解释
  • 后端把同一批候选量表转成 recommendedCards

这意味着:

  • LLM 负责理解和表达
  • RAG 负责从真实量表库里找候选
  • 前端卡片来自后端结构化结果
  • replyrecommendedCards 使用同一批 searchScaleRagEntries top-k 结果,不再分别检索

这比单纯让 LLM 自由发挥更稳定,也比旧版 exact keyword matching 更能处理自然语言。

这次还移除了 recommendedCards 的二次检索逻辑。此前 LLM 回答会使用 RAG 检索出的候选量表,但推荐卡片可能又基于用户原始输入或 LLM 回复重新搜索一次,导致“回答里推荐的是时间管理,卡片却显示更宽泛量表”的不一致。现在 /api/chat 只检索一次:同一批 retrievedEntries 同时用于 DeepSeek prompt 和前端推荐卡片。

配置与部署影响

新增了环境变量示例:

  • .env.local.example

内容是:

DEEPSEEK_API_KEY=your_deepseek_api_key_here

因为新增了 /api/chat,项目不能再用纯 static export 模式。next.config.mjs 中移除了:

output: 'export'

并保留服务端运行模式,让 Next.js API Route 可以处理 DeepSeek 请求和 CSV 文件读取。

验证结果

已完成以下验证:

  • npx tsc --noEmit 通过
  • npm run build 通过
  • /api/chat 在 build 输出中显示为 dynamic server route
  • CSV 解析结果为 212 条量表
  • 本地检索样例通过:
  • 焦虑 压力
  • 这里有什么量表
  • 时间管理
  • 时间管理相关的量表
  • 有没有关于时间管理的量表
  • 注意力 分心
  • DASS
  • ADHD
  • intelligence

当前项目文件路径树

以下文件树排除了 .git.nextnode_modules.idea 等本地或生成目录。

.
├── DEPLOYMENT_CHECKLIST.md
├── Dockerfile
├── EQAI心理量表总览表.csv
├── FEATURES.md
├── QUICKSTART.md
├── README.md
├── UPDATES.md
├── VERCEL_DEPLOYMENT.md
├── docker-compose.ecs.yml
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── tailwind.config.ts
├── tsconfig.json
├── vercel.json
├── messages
│   ├── en.json
│   └── zh.json
├── public
│   └── logo.jpeg
├── dataset
│   ├── assessment_data.json
│   ├── 测评总览 (ZF Feb 25 2026).csv
│   ├── 测评总览 (ZF Feb 25 2026).xlsx
│   └── 测评总览 (ZF Feb 25 2026) (1).xlsx
├── docs
│   ├── product-direction.md
│   ├── devlog
│   │   ├── 2026-05-29-directory-platform-plan.md
│   │   ├── 2026-05-30-product-update-brief.md
│   │   ├── 2026-05-31-future-roadmap.md
│   │   ├── 2026-06-07-product-work-note.md
│   │   ├── 2026-06-13-weekly.md
│   │   ├── 2026-06-14-anura-daily.md
│   │   ├── 2026-06-23-anura-mobile-report.md
│   │   ├── 2026-06-24-anura-deployment-report.md
│   │   ├── 2026-06-24-eqai-static-domain-launch-report.md
│   │   ├── 2026-06-26-webdev-anura-domain-deployment.md
│   │   ├── 2026-06-27-anura-prototype-and-websocket-update.md
│   │   └── 2026-07-03-deepseek-chat-rag-scale-recommendation.md
│   └── review-assets
│       ├── README.md
│       ├── recordings
│       │   └── mvp-walkthrough.webm
│       └── screenshots
│           ├── desktop-en-assessments.png
│           ├── desktop-en-contact.png
│           ├── desktop-en-home.png
│           ├── desktop-en-review.png
│           ├── mobile-zh-home.png
│           └── mobile-zh-review.png
├── src
│   ├── app
│   │   ├── globals.css
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   ├── api
│   │   │   └── chat
│   │   │       └── route.ts
│   │   └── [locale]
│   │       ├── layout.tsx
│   │       ├── page.tsx
│   │       ├── about
│   │       │   └── page.tsx
│   │       ├── assessments
│   │       │   ├── page.tsx
│   │       │   └── [scaleCode]
│   │       │       ├── page.tsx
│   │       │       ├── result
│   │       │       │   └── page.tsx
│   │       │       └── take
│   │       │           └── page.tsx
│   │       ├── contact
│   │       │   └── page.tsx
│   │       ├── devlog
│   │       │   ├── page.tsx
│   │       │   └── [slug]
│   │       │       └── page.tsx
│   │       ├── kid
│   │       │   └── page.tsx
│   │       ├── login
│   │       │   └── page.tsx
│   │       ├── me
│   │       │   └── assessments
│   │       │       └── page.tsx
│   │       ├── personal
│   │       │   └── page.tsx
│   │       ├── pet
│   │       │   └── page.tsx
│   │       ├── privacy
│   │       │   └── page.tsx
│   │       ├── review
│   │       │   └── page.tsx
│   │       ├── terms
│   │       │   └── page.tsx
│   │       └── work
│   │           └── page.tsx
│   ├── components
│   │   ├── AssessmentCard.tsx
│   │   ├── ChatBox.tsx
│   │   ├── ContactForm.tsx
│   │   ├── FeatureCard.tsx
│   │   ├── Footer.tsx
│   │   ├── HeroSection.tsx
│   │   ├── LanguageToggle.tsx
│   │   ├── LoginForm.tsx
│   │   ├── Navigation.tsx
│   │   ├── PageChrome.tsx
│   │   ├── ScaleSubmitButton.tsx
│   │   ├── StaticAssessmentCatalog.tsx
│   │   ├── StaticRecordsList.tsx
│   │   ├── StaticScaleResult.tsx
│   │   └── StaticScaleTake.tsx
│   ├── i18n
│   │   ├── locales.ts
│   │   └── request.ts
│   └── lib
│       ├── assessmentDirectory.ts
│       ├── deepseek.ts
│       ├── devlog.ts
│       ├── mvpScales.ts
│       ├── scaleRag.ts
│       ├── staticRecords.ts
│       └── supabase
│           ├── browser.ts
│           └── server.ts
├── backend
│   ├── requirements.txt
│   ├── app
│   │   ├── __init__.py
│   │   ├── config.py
│   │   └── db
│   │       ├── __init__.py
│   │       ├── queries.py
│   │       └── supabase_client.py
│   ├── ingestion
│   │   ├── __init__.py
│   │   ├── assessment_directory_seed.sql
│   │   ├── contact_submissions.sql
│   │   ├── embedder_batch.py
│   │   ├── generate_assessment_directory_seed.mjs
│   │   ├── init_supabase_safe.sql
│   │   ├── mvp_rls_smoke.sql
│   │   ├── mvp_scale_records.sql
│   │   ├── pipeline.py
│   │   ├── schema.sql
│   │   ├── supabase_functions.sql
│   │   ├── loaders
│   │   │   ├── __init__.py
│   │   │   └── dataset_loader.py
│   │   └── transformers
│   │       ├── __init__.py
│   │       └── normalizer.py
│   └── tests
│       ├── __init__.py
│       └── test_ingestion.py
└── tests
    ├── accessibility-baseline.test.mjs
    ├── assessment-directory.test.mjs
    ├── chatbox-visibility.test.mjs
    ├── check-mvp-readiness.mjs
    ├── contact-submissions.test.mjs
    ├── mvp-scale-records.test.mjs
    ├── privacy-terms.test.mjs
    ├── review-page.test.mjs
    ├── smoke-contact.mjs
    └── smoke-mvp-auth.mjs