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 负责从真实量表库里找候选
- 前端卡片来自后端结构化结果
reply和recommendedCards使用同一批searchScaleRagEntriestop-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 条量表
- 本地检索样例通过:
焦虑 压力这里有什么量表时间管理时间管理相关的量表有没有关于时间管理的量表注意力 分心DASSADHDintelligence
当前项目文件路径树
以下文件树排除了 .git、.next、node_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