2 阿飞哥 7小时前 66次点击
各位社区的小伙伴们,大家好!我是飞言 TTS 的站长阿飞哥,在进入正题之前,先简单的向大家介绍一下我们飞言 TTS。
飞言 TTS 是一款在线文字转语音(TTS)应用,集成 MiniMax、小米 MiMo 等主流语音大模型。无需高性能电脑、无需自备 API Key,登录即可一键合成语音,支持声音复刻、长文本听书与公众号文章朗读。App 更可作为系统级语音引擎,供开源阅读、读屏软件等第三方应用直接调用,是听书、小说朗读、短视频配音与无障碍朗读的好帮手。
进入正题。我为什么放着好好的低代码 Agent 不用,偏要自己开发呢?一句话概括就是,自己动手能自己说了算。腾讯起点扣子,他们虽然方便,但是他们做出来的客服只能回答静态内容,想要回答动态内容很麻烦。与其这样子,还不如自己写一个框架出来呢。而且计费上,第三方低代码平台费用非常的贵。我自己调用 Deepseek,价格几乎就跟白菜价似的。大家可能好奇我们是怎么让 Deepseek 回答我们给它的东西呢?是一股脑的塞进系统提示词吗?不是,以下是详细的技术原理。
DeepSeek 可插拔知识库客服 Agent 技术文档
技术栈:React 19 + TypeScript + Vite + Tailwind + shadcn/ui(前端);Hono + tRPC 11 + Drizzle ORM + MySQL(后端,webapp-building/backend-building 技能脚手架)。所有第三方 API Key 只存后端 .env,前端永不可见。
1. 总体架构
用户(浏览器) ├─ 文字提问 ──► 后端 tRPC chat.send ──► DeepSeek deepseek-v4-flash │ │ tool_calls: query_knowledge_base │ ▼ │ 后端查 MySQL 知识库 ──► 以 tool 消息回填 ──► 模型生成最终回答 ├─ 语音提问 ──► 浏览器录音(16kHz WAV) ──► 后端 ──► 阿里云 Fun-ASR (WebSocket 双工) ──► 识别文本 ──► 走上面同一聊天链路 ├─ 收到回答 ──► stripMarkdown 清洗 ──► 显示 + 调 MiniMax T2A 合成 mp3 ──► 自动播放(被浏览器拦截则显示播放按钮) └─ 管理页 ──► 知识库 CRUD / 图片上传(deepseek-v4-flash-vision-exp 提取) / 一键重置
数据流核心原则:知识库不写在提示词里。模型通过 Function Call 每次实时查库,因此知识库随时增删改立即生效,无需改任何代码或提示词。
2. DeepSeek 聊天链路(核心)
2.1 接口
Base URL:https://api.deepseek.com,OpenAI 兼容 POST /chat/completions
Header:Authorization: Bearer
聊天模型:deepseek-v4-flash(支持 tools/function calling)
环境变量:DEEPSEEK_API_KEY、DEEPSEEK_BASE_URL、DEEPSEEK_CHAT_MODEL
2.2 Function Call 工具定义
{ "type": "function", "function": { "name": "query_knowledge_base", "description": "查询客服知识库的全部最新内容(服务项目、价格、科普说明、常见问题等)。回答任何业务问题前必须先调用此工具。", "parameters": { "type": "object", "properties": {}, "required": [] } }}
2.3 调用循环(伪代码,最多 5 轮)
messages = [system(人设+规则), ...历史(最近12条)]loop: resp = POST /chat/completions { model, messages, tools:[KB_TOOL], tool_choice:"auto", max_tokens:2048 } if resp.message.tool_calls 非空: 把 assistant 消息(含 tool_calls)原样 push 进 messages 对每个 tool_call: 查数据库 → push { role:"tool", tool_call_id, content:"以下是知识库当前全部内容(以此为唯一事实依据):\n\n" } continue else: return message.content
要点:tool 消息里明确标注”以此为唯一事实依据”,配合系统提示词实现反幻觉。
2.4 系统提示词(反幻觉关键,业务无关可直接复用)
人设一句 + 硬规则: 1. 回答业务问题前必须先调用 query_knowledge_base,不凭记忆回答(知识库随时会被改)。 2. 知识库里没有的项目/价格绝不编造;被问到时温柔明确告知”我是客服 XX,只能回答知识库里的内容”。 3. 根据知识库实际主题自适应服务范围(按摩店知识库就只答按摩店,换成电商就只答电商)。 4. 用户问”XX 是什么”时,结合知识库科普文案用客服口吻解释。 5. 回答口语化、简短,像微信聊天。
2.5 知识库数据模型
MySQL 表 kb_items:id serial PK、title varchar(255)、content text、sort int、updated_at timestamp。 - listKbItems():按 sort 升序全量读出。 - replaceKbItems(items):删全表再批量插入(管理页”钉牢保存”的语义就是整体替换)。 - formatKbForModel():拼成 【条目N】标题\n内容 的纯文本喂给 tool 消息。 - 初始快照存为代码常量 db/initialKb.ts(不可变),seed 与”恢复初始化设置”接口共用,重置 = replaceKbItems(initialKbItems)。
3. 图片提取知识库(视觉模型)
模型:deepseek-v4-flash-vision-exp(仅此模型接受图片,其他模型传图会 400)。
传图方式(OpenAI 兼容):content 用块数组,图片块为 {"type":"image_url","image_url":{"url":"data:image/jpeg;base64,..."}},图片只能出现在 user 消息。也支持公网 URL 和 Files API file_id。
限制:JPEG/PNG/GIF/WebP;base64/URL 单图 ≤32MiB;请求体 ≤48MiB;单请求 ≤600 图;单边 ≤8192px。
提取提示词要点:要求输出严格 标题:内容 一行一条、只输出条目不要 Markdown、价格数字与图片完全一致、不编造。前端按 ^(.{1,30}?)[::](.+)$ 逐行解析成条目,追加到编辑列表并滚动定位。
4. 语音识别:阿里云百炼 Fun-ASR
接入:DashScope WebSocket 双工协议,URL wss://dashscope.aliyuncs.com/api-ws/v1/inference,Header Authorization: bearer ,模型 fun-asr-realtime。
环境变量:ALIBABA_ASR_API_KEY、ALIBABA_ASR_WS_URL、ALIBABA_ASR_MODEL。
前端录音:Web Audio API(AudioContext + ScriptProcessor + AnalyserNode),采集后降采样为 16kHz 单声道 16bit PCM,手工拼 44 字节 WAV 头,base64 传给后端。AnalyserNode 的时域数据 RMS 即实时音量,驱动声纹条动画。交互为”点击开始 / 再点停止”(不用长按)。
后端转发流程:
建连后发送 run-task:{header:{action:"run-task",task_id:,streaming:"duplex"},payload:{task_group:"audio",task:"asr",function:"recognition",model,parameters:{format:"wav",sample_rate:16000},input:{}}}
收到 task-started 后,按 100ms/帧(3200 字节)推送二进制音频,发完发送 finish-task。
收到 result-generated(含 payload.output.sentence,同一句中间结果多次推送,需按 begin_time 去重并做”前缀被长句覆盖则丢弃”后处理,否则文本重复);收到 task-finished 后拼接全部句子返回。
30 秒超时兜底。
识别出的文本直接走第 2 节的聊天链路。
5. 语音合成:MiniMax T2A
接口:POST https://api.minimaxi.com/v1/t2a_v2(注意域名是 minimaxi.com),Header Authorization: Bearer 。
环境变量:MINIMAX_API_KEY、MINIMAX_TTS_URL、MINIMAX_VOICE_ID。
请求体关键字段:
{ "model": "speech-2.8-hd", "text": "", "stream": false, "voice_setting": { "voice_id": "female-tianmei", "speed": 1, "vol": 1, "pitch": 0 }, "audio_setting": { "sample_rate": 32000, "bitrate": 128000, "format": "mp3", "channel": 1 }, "language_boost": "Chinese", "output_format": "hex"}
音色:甜美女性音色 female-tianmei(系统音色;另有 female-tianmei-jingpin 为 beta 精品版)。完整系统音色列表可用 POST /v1/get_voice {"voice_type":"system"} 拉取。
返回:data.audio 为 hex 编码的 mp3,后端转 base64 回前端,前端 new Audio("data:audio/mp3;base64,...") 播放。
成功判据:base_resp.status_code === 0 且 data.audio 非空。
前端策略:每条客服回复渲染后尝试自动播放;浏览器自动播放策略拦截(play() 抛错)时,在该消息下方显示显眼的”▶ 点击播放语音回复”按钮手动触发。朗读前先过 stripMarkdown。
6. 前端清洗层(stripMarkdown)
模型输出展示与朗读前统一剥掉 Markdown 记号:**/__ 加粗、*/_ 斜体、# 标题、` 代码、> 引用、~~ 删除线、链接/图片只留可见文字、列表符号转 · 和 、、压缩多余空行。
7. 交互细节清单
聊天等待动画:“正在翻阅知识库核对资料 📖”——如实反映 Function Call 查库过程。
保存知识库动画:📌 钉子钉住清单的 CSS keyframes 动画(@keyframes pin-drop),并支持 prefers-reduced-motion 降级。
无障碍:skip link、aria-label/aria-live、44px 触控目标、focus-visible 高亮环。
管理页:条目 CRUD + 上下移动 + 图片提取 + 提取后一键跳转定位(scrollIntoView + 高亮描边)+ 一键重置(初始快照不可变)。
注意:Node 端须在模块顶层 import "dotenv/config" 且惰性读取(用函数内 process.env),避免模块加载顺序导致读到空值。真实 Key 只保存在部署环境 .env 中,本交接文档不含明文 Key,请向项目所有者(阿飞哥)索取。
积分:4538