- 第一篇《产品设计与用户体验》,面向产品经理、设计师和业务方,聚焦产品定位、核心功能、用户旅程和交互体验,并用可视化图表帮助理解。
- 第二篇《技术架构与部署实践》,面向工程师、运维和架构师,深入技术栈、数据模型、关键算法实现、API设计、部署运维及演进规划。
第二篇:技术架构与部署实践
1. 技术概览:小兰的"骨骼"与"血液"
小兰是一个全栈应用,采用现代Web技术栈构建,整体架构如下:
#mermaid-svg-LJfpHO2XSXTfj5Mv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LJfpHO2XSXTfj5Mv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LJfpHO2XSXTfj5Mv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LJfpHO2XSXTfj5Mv .error-icon{fill:#552222;}#mermaid-svg-LJfpHO2XSXTfj5Mv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LJfpHO2XSXTfj5Mv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LJfpHO2XSXTfj5Mv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LJfpHO2XSXTfj5Mv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LJfpHO2XSXTfj5Mv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LJfpHO2XSXTfj5Mv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LJfpHO2XSXTfj5Mv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LJfpHO2XSXTfj5Mv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LJfpHO2XSXTfj5Mv .marker.cross{stroke:#333333;}#mermaid-svg-LJfpHO2XSXTfj5Mv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LJfpHO2XSXTfj5Mv p{margin:0;}#mermaid-svg-LJfpHO2XSXTfj5Mv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LJfpHO2XSXTfj5Mv .cluster-label text{fill:#333;}#mermaid-svg-LJfpHO2XSXTfj5Mv .cluster-label span{color:#333;}#mermaid-svg-LJfpHO2XSXTfj5Mv .cluster-label span p{background-color:transparent;}#mermaid-svg-LJfpHO2XSXTfj5Mv .label text,#mermaid-svg-LJfpHO2XSXTfj5Mv span{fill:#333;color:#333;}#mermaid-svg-LJfpHO2XSXTfj5Mv .node rect,#mermaid-svg-LJfpHO2XSXTfj5Mv .node circle,#mermaid-svg-LJfpHO2XSXTfj5Mv .node ellipse,#mermaid-svg-LJfpHO2XSXTfj5Mv .node polygon,#mermaid-svg-LJfpHO2XSXTfj5Mv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LJfpHO2XSXTfj5Mv .rough-node .label text,#mermaid-svg-LJfpHO2XSXTfj5Mv .node .label text,#mermaid-svg-LJfpHO2XSXTfj5Mv .image-shape .label,#mermaid-svg-LJfpHO2XSXTfj5Mv .icon-shape .label{text-anchor:middle;}#mermaid-svg-LJfpHO2XSXTfj5Mv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LJfpHO2XSXTfj5Mv .rough-node .label,#mermaid-svg-LJfpHO2XSXTfj5Mv .node .label,#mermaid-svg-LJfpHO2XSXTfj5Mv .image-shape .label,#mermaid-svg-LJfpHO2XSXTfj5Mv .icon-shape .label{text-align:center;}#mermaid-svg-LJfpHO2XSXTfj5Mv .node.clickable{cursor:pointer;}#mermaid-svg-LJfpHO2XSXTfj5Mv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LJfpHO2XSXTfj5Mv .arrowheadPath{fill:#333333;}#mermaid-svg-LJfpHO2XSXTfj5Mv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LJfpHO2XSXTfj5Mv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LJfpHO2XSXTfj5Mv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LJfpHO2XSXTfj5Mv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LJfpHO2XSXTfj5Mv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LJfpHO2XSXTfj5Mv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LJfpHO2XSXTfj5Mv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LJfpHO2XSXTfj5Mv .cluster text{fill:#333;}#mermaid-svg-LJfpHO2XSXTfj5Mv .cluster span{color:#333;}#mermaid-svg-LJfpHO2XSXTfj5Mv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LJfpHO2XSXTfj5Mv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LJfpHO2XSXTfj5Mv rect.text{fill:none;stroke-width:0;}#mermaid-svg-LJfpHO2XSXTfj5Mv .icon-shape,#mermaid-svg-LJfpHO2XSXTfj5Mv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LJfpHO2XSXTfj5Mv .icon-shape p,#mermaid-svg-LJfpHO2XSXTfj5Mv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LJfpHO2XSXTfj5Mv .icon-shape .label rect,#mermaid-svg-LJfpHO2XSXTfj5Mv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LJfpHO2XSXTfj5Mv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LJfpHO2XSXTfj5Mv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LJfpHO2XSXTfj5Mv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 数据层
核心服务
边缘代理层
客户端
HTTP
反向代理
透传
调用
读写
可选
默认兜底
浏览器
Next.js 16 + React 19
Caddy 反向代理
端口 81
Next.js 主服务
standalone
API Routes + RSC
可选微服务
mini-services
自定义LLM
OpenAI兼容接口
SQLite数据库
Prisma ORM
z-ai-web-dev-sdk
1.1 技术栈速览
| 类别 | 选型 | 版本/说明 |
|---|---|---|
| Web框架 | Next.js (App Router) | 16.1.1,output:"standalone" |
| 前端 | React + TypeScript | 19.0 / 5.x,路径别名 @/* |
| 样式 | Tailwind CSS + shadcn/ui | 4.x,基于Radix UI |
| 动效 | Framer Motion | 12.23.2 |
| 状态管理 | TanStack Query | 5.82.0 |
| 数据库 | SQLite + Prisma | 单文件 db/custom.db |
| 运行时 | Bun | 开发/生产启动/脚本 |
| 默认LLM SDK | z-ai-web-dev-sdk | 0.0.18(兜底) |
| 边缘代理 | Caddy | 自动HTTPS/反向代理 |
| 构建工具 | Bun + Next.js内置 | 生成standalone产物 |
详细依赖见
package.json,锁文件由 Bun 维护。
2. 数据模型:持久化的"记忆"和"个性"
数据库使用SQLite,通过Prisma ORM描述。共有7张核心表,覆盖用户、会话、消息、记忆、性格、情绪日志、安全事件和LLM配置。
2.1 ER图概览
#mermaid-svg-9Fud5fDMqM27xxYs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9Fud5fDMqM27xxYs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9Fud5fDMqM27xxYs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9Fud5fDMqM27xxYs .error-icon{fill:#552222;}#mermaid-svg-9Fud5fDMqM27xxYs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9Fud5fDMqM27xxYs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9Fud5fDMqM27xxYs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9Fud5fDMqM27xxYs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9Fud5fDMqM27xxYs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9Fud5fDMqM27xxYs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9Fud5fDMqM27xxYs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9Fud5fDMqM27xxYs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9Fud5fDMqM27xxYs .marker.cross{stroke:#333333;}#mermaid-svg-9Fud5fDMqM27xxYs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9Fud5fDMqM27xxYs p{margin:0;}#mermaid-svg-9Fud5fDMqM27xxYs .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-9Fud5fDMqM27xxYs .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-9Fud5fDMqM27xxYs .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-9Fud5fDMqM27xxYs .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-9Fud5fDMqM27xxYs .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-9Fud5fDMqM27xxYs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9Fud5fDMqM27xxYs .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-9Fud5fDMqM27xxYs .node rect,#mermaid-svg-9Fud5fDMqM27xxYs .node circle,#mermaid-svg-9Fud5fDMqM27xxYs .node ellipse,#mermaid-svg-9Fud5fDMqM27xxYs .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9Fud5fDMqM27xxYs .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-9Fud5fDMqM27xxYs .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-9Fud5fDMqM27xxYs .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9Fud5fDMqM27xxYs .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-9Fud5fDMqM27xxYs .edgeLabel .label text{fill:#333;}#mermaid-svg-9Fud5fDMqM27xxYs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} has
owns
generates
triggers
has one
contains
configured by
User
string
id
string
nickname
string
ageGroup
string
avatarColor
Conversation
string
id
string
userId
string
scenario
string
title
int
messageCount
Memory
string
id
string
userId
string
type
string
content
string
summary
json
keywords
json
embedding
float
importance
int
accessCount
boolean
pinned
EmotionLog
string
id
string
userId
string
emotion
float
score
string
scenario
SafetyEvent
string
id
string
userId
string
type
string
severity
string
triggerText
string
action
boolean
resolved
PersonalityProfile
string
userId
float
warmth
float
playfulness
float
formality
float
intimacy
float
patience
float
humor
int
interactions
string
stage
Message
string
id
string
conversationId
string
role
string
content
string
modelTier
string
emotion
float
emotionScore
boolean
safetyFlag
boolean
memoryUsed
int
tokens
LlmConfig
string
tier
string
baseUrl
string
apiKey
string
modelId
float
temperature
int
maxTokens
boolean
enabled
boolean
lastTestOk
Tier
2.2 关键表说明
- User :单用户演示模式,
/api/init自动创建首位用户。 - Conversation :每次新会话一行,
title取自首条消息前20字。 - Message:记录每条消息的档位、情绪、安全标志、是否使用记忆、token数等元数据。
- Memory :长期记忆存储,
embedding字段以JSON存储TF-IDF向量({token: tf}),支持轻量级语义检索。 - PersonalityProfile:每用户一条,6维性格+累计交互数+关系阶段。
- EmotionLog / SafetyEvent:用于后续趋势分析和审计。
- LlmConfig:4个档位独立配置,API Key存储并掩码返回。
2.3 长期记忆的TF-IDF实现
由于SQLite无原生向量支持,小兰采用TF-IDF + JSON向量的轻量方案:
- 分词:英文按单词(>2字符),中文按2-gram/3-gram,去除停用词。
- 计算TF :词频统计,存为
Record<token, freq>。 - 检索:对查询和每条记忆取并集键,计算余弦相似度。
- 综合得分 :
score = 0.6 * 相似度 + 0.25 * 重要度 + 0.15 * 时间衰减(90天半衰期)。 - 过滤:只在重要度Top-200的记忆上检索,避免全表扫描。
该方案在百级记忆规模下表现良好,未来可平滑迁移至 sqlite-vss 或外置向量库。
3. 核心算法实现:从输入到回复的"脑内风暴"
3.1 情绪检测(emotion.ts)
- 关键词词典 :预定义6类情绪关键词(
happy/excited/sad/angry/anxious/lonely)。 - 强度曲线:单关键词强度0.5,2词0.65,3词0.8,4词以上0.9(防止单词过度升级)。
- 标点加成 :
!提升excited,...提升sad,?提升anxious,连续!!提升angry(各+0.08)。 - 危机检测:优先匹配自伤/自杀/暴力等高危词,强制触发安全模式。
3.2 路由决策(router.ts)
路由规则按优先级从高到低:
- 安全覆盖 :若
forceTier为safety或检测到危机,直接选safety。 - 深度路由:情绪强度≥0.85,或负向强度≥0.75,或消息≥250字 → deep。
- 平衡路由:负向强度0.4~0.75,或多轮≥5轮且强度≥0.35 → balanced。
- 默认:light(日常闲聊)。
3.3 记忆检索与提取(memory.ts)
- 检索 :
retrieveMemories(userId, query, k=3)在Top-200重要度记忆上跑余弦相似度,返回Top-3,并累加accessCount。 - 提取:每轮对话后,用6类关键词模式(如"我喜欢...""我记得...")抽取新记忆,限制长度4~120字符,最多2条。
- 去重:按内容全等去重,若已存在则重要度+0.05(封顶1)。
3.4 性格自适应(personality.ts)
6维参数每轮按规则微调:
| 维度 | 调整 | 触发条件 |
|---|---|---|
| 亲密 | +0.04/+0.03/+0.015 | 用户问起小兰/分享故事/消息长 |
| 活泼 | +0.02(快乐)/ -0.03(负面) | 情绪类别 |
| 幽默 | +0.02(快乐)/ -0.02(负面) | 情绪类别 |
| 正式 | -0.015(放松)/ +0.01(负面) | 中性/快乐 vs 难过/焦虑 |
| 温暖 | +0.005(长期)/ +0.02(负面) | 长期累积,负面时加速 |
| 耐心 | +0.01(负面) | 负面情绪下上升 |
interactions每次+1;stage根据累计交互数推断(<10初识,<50熟悉,<150亲密,否则知己)。- 性格最终转换为自然语言描述,植入系统提示词,由LLM内化为语气。
3.5 LLM调用与降级链(llm-client.ts + orchestrator.ts)
三级降级保证可用性:
#mermaid-svg-XKfH8nSnJBbwelhZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XKfH8nSnJBbwelhZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XKfH8nSnJBbwelhZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XKfH8nSnJBbwelhZ .error-icon{fill:#552222;}#mermaid-svg-XKfH8nSnJBbwelhZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XKfH8nSnJBbwelhZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XKfH8nSnJBbwelhZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XKfH8nSnJBbwelhZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XKfH8nSnJBbwelhZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XKfH8nSnJBbwelhZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XKfH8nSnJBbwelhZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XKfH8nSnJBbwelhZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XKfH8nSnJBbwelhZ .marker.cross{stroke:#333333;}#mermaid-svg-XKfH8nSnJBbwelhZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XKfH8nSnJBbwelhZ p{margin:0;}#mermaid-svg-XKfH8nSnJBbwelhZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XKfH8nSnJBbwelhZ .cluster-label text{fill:#333;}#mermaid-svg-XKfH8nSnJBbwelhZ .cluster-label span{color:#333;}#mermaid-svg-XKfH8nSnJBbwelhZ .cluster-label span p{background-color:transparent;}#mermaid-svg-XKfH8nSnJBbwelhZ .label text,#mermaid-svg-XKfH8nSnJBbwelhZ span{fill:#333;color:#333;}#mermaid-svg-XKfH8nSnJBbwelhZ .node rect,#mermaid-svg-XKfH8nSnJBbwelhZ .node circle,#mermaid-svg-XKfH8nSnJBbwelhZ .node ellipse,#mermaid-svg-XKfH8nSnJBbwelhZ .node polygon,#mermaid-svg-XKfH8nSnJBbwelhZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XKfH8nSnJBbwelhZ .rough-node .label text,#mermaid-svg-XKfH8nSnJBbwelhZ .node .label text,#mermaid-svg-XKfH8nSnJBbwelhZ .image-shape .label,#mermaid-svg-XKfH8nSnJBbwelhZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-XKfH8nSnJBbwelhZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XKfH8nSnJBbwelhZ .rough-node .label,#mermaid-svg-XKfH8nSnJBbwelhZ .node .label,#mermaid-svg-XKfH8nSnJBbwelhZ .image-shape .label,#mermaid-svg-XKfH8nSnJBbwelhZ .icon-shape .label{text-align:center;}#mermaid-svg-XKfH8nSnJBbwelhZ .node.clickable{cursor:pointer;}#mermaid-svg-XKfH8nSnJBbwelhZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XKfH8nSnJBbwelhZ .arrowheadPath{fill:#333333;}#mermaid-svg-XKfH8nSnJBbwelhZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XKfH8nSnJBbwelhZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XKfH8nSnJBbwelhZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XKfH8nSnJBbwelhZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XKfH8nSnJBbwelhZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XKfH8nSnJBbwelhZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XKfH8nSnJBbwelhZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XKfH8nSnJBbwelhZ .cluster text{fill:#333;}#mermaid-svg-XKfH8nSnJBbwelhZ .cluster span{color:#333;}#mermaid-svg-XKfH8nSnJBbwelhZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XKfH8nSnJBbwelhZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XKfH8nSnJBbwelhZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-XKfH8nSnJBbwelhZ .icon-shape,#mermaid-svg-XKfH8nSnJBbwelhZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XKfH8nSnJBbwelhZ .icon-shape p,#mermaid-svg-XKfH8nSnJBbwelhZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XKfH8nSnJBbwelhZ .icon-shape .label rect,#mermaid-svg-XKfH8nSnJBbwelhZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XKfH8nSnJBbwelhZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XKfH8nSnJBbwelhZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XKfH8nSnJBbwelhZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 成功
失败
成功
失败
用户配置的Custom LLM
返回内容
z-ai-web-dev-sdk默认
返回内容 + 标记fallback
模板化固定文案
按情绪/safety模式
前端兜底文案
- 自定义LLM :走标准OpenAI兼容接口,支持超时和AbortSignal,
baseUrl智能补全。 - 内置预设:OpenAI、DeepSeek、智谱、Moonshot、通义、Ollama。
- 固定文案:根据当前情绪或安全模式,生成温暖、安全的模板回复。
4. API设计:前后端通信契约
所有API基于Next.js Route Handlers,运行于Node.js环境,/api/chat 设置 maxDuration=60s。
| 方法 | 路径 | 用途 | 关键输入 | 关键输出 |
|---|---|---|---|---|
| GET | /api/init |
初始化用户 | --- | userId, user |
| POST | /api/chat |
发送消息 | userId, userMessage, scenario, forceTier? |
完整回复 + 路由/情绪/记忆元数据 |
| GET | /api/memory |
获取记忆列表 | userId |
记忆数组 + 分类统计 |
| DELETE | /api/memory |
删除单条记忆 | id |
{ok} |
| GET | /api/personality |
获取性格 | userId |
6维参数 + stage |
| GET | /api/scenario |
获取场景列表 | --- | 4场景(不含systemPrompt) |
| GET | /api/conversations |
获取对话列表或单对话消息 | userId 或 conversationId |
会话列表或消息数组 |
| POST | /api/conversations |
新建对话 | userId, scenario, title? |
conversation |
| GET | /api/llm-config |
获取LLM配置 | --- | 4档位配置(API Key掩码) |
| POST | /api/llm-config |
更新LLM配置 | tier, baseUrl, apiKey, ... |
{ok, config} |
| DELETE | /api/llm-config |
删除某档位配置(恢复默认) | tier |
{ok} |
错误处理统一返回 {error} + HTTP状态码。
5. 构建与部署:从代码到线上服务
5.1 本地开发
bash
# 安装Bun(macOS/Linux)
curl -fsSL https://bun.sh/install | bash
# 安装依赖
bun install
# 初始化数据库(Prisma迁移)
bun run db:push
# 启动开发服务器
bun run dev # 访问 http://localhost:3000
便捷开发脚本 .zscripts/dev.sh 可一键完成上述步骤,并自动启动 mini-services/ 下的子服务。
5.2 生产构建与产物
执行 bun run build 后,生成 .next/standalone/ 目录,包含:
- Node.js独立可运行入口(含
node_modules子集) .next/static/静态资源- 复制
public/到standalone内 - 数据库文件
db/custom.db(构建时打入)
整个目录可以打包为容器镜像,单目录自包含。
5.3 容器化部署(推荐)
Dockerfile 关键步骤:
dockerfile
FROM oven/bun:1 AS build
# 安装依赖,构建
...
FROM oven/bun:1
RUN apt-get update && apt-get install -y caddy
WORKDIR /app
COPY --from=build /home/z/my-project/.next/standalone ./
COPY --from=build /home/z/my-project/.next/static ./.next/static
COPY --from=build /home/z/my-project/public ./public
COPY --from=build /home/z/my-project/db ./db
COPY --from=build /home/z/my-project/Caddyfile ./
COPY .zscripts/start.sh ./
CMD ["./start.sh"]
启动脚本 start.sh:
- 后台启动Next.js standalone(
PORT=3000,DATABASE_URL指向挂载的db)。 - 若有
mini-services-start.sh,后台启动。 - 前台运行Caddy(监听81端口),反向代理到Next.js,并支持
XTransformPort透传到微服务。 - 接收SIGTERM,优雅关闭所有子进程。
数据库挂载 :将 db/custom.db 挂载为Volume,保证持久化。
5.4 反向代理与HTTPS
Caddyfile默认:
:81 {
reverse_proxy localhost:3000
# 支持 XTransformPort 透传
}
若需公网HTTPS,改为域名块,Caddy自动申请证书。
5.5 LLM配置管理
启动后,通过前端"大模型配置"弹窗为4个档位分别配置:
- 选择Provider预设(一键填充baseUrl和modelId)
- 输入API Key(可显示/隐藏)
- 点击"测试连接"验证
- 保存后,
/api/chat优先使用自定义配置,失败则降级默认SDK。
建议安全档(safety)务必配置最稳定的供应商,避免危机时不可用。
5.6 进程管理与监控
- Next.js监听3000,Caddy监听81(对外)。
- 健康检查:
curl -fsS http://localhost:81/。 - 日志输出至stdout,便于容器日志收集。
- 优雅关停:SIGTERM后等待子进程退出最多5秒,超时则KILL。
5.7 Mini-Services扩展机制
mini-services/ 目录下可放置独立的子服务(如WebSocket、定时任务等)。构建器和启动器自动处理:
mini-services-install.sh:每个子目录执行bun installmini-services-build.sh:智能识别入口(src/index.ts/index.ts/*.js),打包为独立JSmini-services-start.sh:后台启动所有打包后的服务
外部访问通过 http://host:81/?XTransformPort=<port> 透传。
6. 质量保障与演进路线
6.1 当前检查
bun run lint:ESLint检查。bun run build:类型检查+生产构建。
6.2 建议补充的测试
| 类型 | 覆盖内容 |
|---|---|
| 单元测试 | 情绪检测、记忆检索/提取、性格更新、路由决策、LLM降级 |
| 集成测试 | /api/chat 全流程,模拟Prisma,断言各档位输出 |
| 端到端 | Playwright覆盖:输入→路由→性格→记忆→头像联动 |
| 安全用例 | 危机关键词样本库,断言safety触发+热线文案 |
| LLM一致性 | 确定性种子prompt,比对不同档位输出语气/长度 |
| 视觉回归 | 头像7种情绪截图对比 |
6.3 监控与告警建议
- 基础指标:CPU/内存、进程存活、HTTP状态码、QPS、P95延迟。
- 业务指标:每次对话LLM耗时、降级次数、安全事件次数、新增记忆数。
- 日志关键词 :
[xiaolan][LLM]行带档位/后端/耗时。
6.4 风险与边界
| 风险 | 缓解措施 |
|---|---|
| LLM幻觉 | 系统提示明确"非专业人士";UI提示"AI参考" |
| 危机检测漏报 | 后续引入专用分类模型;保留事件供人工跟进 |
| SQLite并发写 | 单实例部署;多实例需迁PostgreSQL |
| TF-IDF召回弱 | 百级记忆内可用;规模扩大后切向量库 |
| 性格漂移 | 暴露关键参数到UI,提供"重置性格"按钮 |
| 第三方LLM不可用 | 4级降级链;建议safety档配独立供应商 |
| 法规合规 | ICP/算法备案;隐私政策;未成年人知情同意 |
6.5 未来演进路线
- 近期(1-2周):暗色模式、多用户登录、记忆导出。
- 中期(1-2月):情绪趋势图、记忆衰减策略、OpenTelemetry tracing、隐私友好埋点。
- 远期(3月+):迁移PostgreSQL+pgvector,专用情绪/风险模型,移动端PWA,SaaS化。
附录:常用运维命令速查
bash
# 开发
bun install && bun run db:push && bun run dev
# 构建
bun run build
# 容器内启动
./start.sh
# 查看路由决策日志
grep '\[xiaolan\]\[LLM\]' server.log
# 重置数据库
bun run db:reset
# 生成头像(开发期)
bun scripts/gen-avatars.ts
通过以上两篇文章,从产品体验和技术实现两个维度,完整地认识了"小兰"AI情感陪伴助手。希望这份梳理能帮助您快速上手、深入理解,并顺利部署和扩展这个温暖、智能的陪伴系统。