AI 电子伴侣聊天中的情绪路由:从能回复到会回应
在 AI 电子伴侣产品里,用户来聊天时并不总是在提问题。很多时候,用户真正需要的是被接住、被陪伴、被安慰,或者只是希望对话不要断掉。
例如用户说:
今天真的好累,不想说话。
如果系统直接把这句话丢给大模型,模型可能会给出一大段建议:
你可以先休息一下,调整作息,尝试运动,保持积极心态……
这些话不一定错,但在陪伴场景里,经常不合适。用户可能只是想听一句:
那我就安静陪你一会儿,今天先不用撑得那么辛苦。
这就是情绪路由要解决的问题。
它不是心理诊断,也不是情绪治疗,而是根据用户这一轮聊天里的情绪状态,决定 Agent 应该用什么方式回应:轻松陪聊、温柔安慰、安静陪伴、降温冲突、修复关系,还是给一点实用建议。
这篇文章我们基于已经落地的实现,把 API 子站里的情绪路由完整梳理一下:先用 LangChain 结构化输出 做情绪识别,再用 LangGraph 编排对话理解流程,最后用 代码规则 完成稳定的情绪路由。
当前聊天链路已经不是简单的用户输入 -> LLM 回复,而是被拆成了几层:
01flowchart TD02A["用户发送消息"] --> B["加载 Agent 信息、历史消息、长期记忆"]03B --> C["安全边界判断"]04C --> D{"是否需要拒绝或危机回复"}05D -- "是" --> E["返回安全边界回复"]06D -- "否" --> F["意图判断"]07F --> G["情绪识别"]08G --> H["情绪路由"]09H --> I["写入用户消息 metadata"]10I --> J["注入最终聊天 prompt"]11J --> K["调用 LLM 流式回复"]12K --> L["保存 assistant 消息"]13L --> M["长期记忆抽取"]
我们可以把这条链路拆开理解。安全边界判断先执行,负责回答这句话能不能聊;安全通过以后,意图判断负责分析用户想要什么;接着情绪识别判断用户现在是什么状态;最后情绪路由才决定 Agent 应该怎样回应。
意图和情绪不是一回事。
同样一句:
你说我该不该去找他?
它的意图可能是 relationship_advice,用户想要关系建议。
但情绪可能完全不同。如果用户是冷静的,Agent 可以直接帮她分析;如果用户是委屈的,Agent 应该先安抚再建议;如果用户是愤怒的,Agent 应该先降温,避免火上浇油;如果用户是焦虑的,Agent 就要减少绝对化判断。
所以不能只做意图判断。意图决定做什么,情绪决定怎么做。
这也是本次实现里把流程拆成:
1classifyIntent -> detectEmotion -> routeEmotion
而不是把它们揉在一个大 prompt 里的原因。
这一版情绪路由有几个明确取舍。
第一,情绪识别交给 LLM。
情绪是语义理解问题,尤其在中文聊天里,很多表达很含蓄。比如算了、没事、你忙吧,可能是真的平静,也可能是失望或拉开距离。让 LLM 结合上下文判断会更合适。
第二,情绪路由使用代码规则。
路由是产品策略问题。什么时候给建议,什么时候不追问,什么时候短回复,什么时候降温,这些应该稳定可控。完全交给 LLM 容易漂。
第三,不做心理诊断。
系统只判断当前对话情绪和陪伴策略,不给用户贴临床标签,也不要把普通抱怨夸大成危机。
第四,失败不影响聊天。
情绪识别失败时,系统使用兜底策略继续回复,而不是让这一轮聊天失败。
情绪识别的输出不是一个简单字符串,而是结构化对象:
01const ConversationEmotionSchema = z.object({02primaryEmotion: z.enum([03'neutral',04'happy',05'tired',06'lonely',07'sad',08'anxious',09'angry',10'jealous',11'embarrassed',12'affectionate',13'playful',14'confused',15'disappointed',16'stressed',17'hurt',18]),19secondaryEmotions: z.array(z.string().trim().min(1).max(40)).max(3),20intensity: z.number().min(0).max(1),21valence: z.enum(['positive', 'neutral', 'negative', 'mixed']),22arousal: z.enum(['low', 'medium', 'high']),23needsComfort: z.boolean(),24needsDeescalation: z.boolean(),25needsClarification: z.boolean(),26emotionalCue: z.string().trim().max(300),27replyTone: z.enum([28'light',29'warm',30'soft',31'playful',32'calm',33'serious',34'reassuring',35'apologetic',36]),37})
这里的字段不只是为了展示分类结果。primaryEmotion 表示主情绪,比如疲惫、孤独、焦虑、开心、暧昧;secondaryEmotions 最多保存三个次要情绪;intensity 表示情绪强度,范围是 0 到 1;valence 记录情绪倾向,可能是正向、中性、负向或混合;arousal 表示情绪激活程度,也就是低、中、高。
后面的 needsComfort、needsDeescalation、needsClarification 会直接影响回复策略。比如要不要先安慰,要不要降温,要不要轻问一句确认。replyTone 则给后续 prompt 一个建议语气。
其中 arousal 很有用。用户难过但平静和愤怒且激动都可能是负面情绪,但回复策略完全不同。
情绪识别之后,会生成一个路由结果:
01const EmotionRouteSchema = z.object({02route: z.enum([03'light_companion',04'warm_comfort',05'deep_comfort',06'playful_flirt',07'calm_deescalation',08'relationship_repair',09'gentle_clarification',10'practical_support',11'quiet_presence',12]),13responseLength: z.enum(['very_short', 'short', 'medium', 'long']),14shouldAskQuestion: z.boolean(),15shouldGiveAdvice: z.boolean(),16shouldUsePetName: z.boolean(),17shouldMirrorEmotion: z.boolean(),18routeGuidance: z.string().trim().max(600),19})
这一步已经更接近最终回复策略了。
例如 quiet_presence 表示用户不想多说,只需要低压力陪伴;warm_comfort 适合轻中度负面情绪;deep_comfort 会更认真地承接情绪;calm_deescalation 用在用户生气、冲突、激动时,先把情绪降下来;relationship_repair 用来处理用户对 Agent 不满,或者对话关系出现误会的情况。除此之外,playful_flirt 负责轻松暧昧互动,practical_support 面向用户确实想解决问题的场景,gentle_clarification 则在意图或情绪不清时轻轻追问。
相比只给模型一句温柔一点,这个路由结构更可控。它会明确告诉最终回复模型:回复要多长,要不要追问,要不要给建议,要不要镜像用户情绪,以及这一轮到底应该走哪种陪伴策略。
情绪识别失败时不能影响聊天,所以实现里定义了兜底对象:
01const fallbackEmotion: ConversationEmotion = {02primaryEmotion: 'neutral',03secondaryEmotions: [],04intensity: 0.3,05valence: 'neutral',06arousal: 'medium',07needsComfort: false,08needsDeescalation: false,09needsClarification: true,10emotionalCue: '情绪识别暂时不可用,采用中性陪伴策略。',11replyTone: 'warm',12}1314const fallbackEmotionRoute: EmotionRoute = {15route: 'gentle_clarification',16responseLength: 'short',17shouldAskQuestion: true,18shouldGiveAdvice: false,19shouldUsePetName: false,20shouldMirrorEmotion: false,21routeGuidance: '先温和承接,再用一个轻问题确认用户想继续聊什么。',22}
这里的兜底策略很保守:先承接,再问一个轻问题,不直接给建议。
对于电子伴侣场景,这是比较安全的默认值。它不会突然进入说教模式,也不会强行暧昧。
情绪识别 prompt 的关键是:明确告诉模型不要回复用户,也不要做诊断。
01const conversationEmotionPrompt = ChatPromptTemplate.fromMessages([02[03'system',04[05'你是 AI 电子伴侣聊天产品的情绪识别器。',06'你的任务不是诊断用户,也不是回复用户,而是判断当前这轮聊天中用户表现出的情绪状态和陪伴需求。',07'必须结合用户输入、最近对话、长期记忆、安全边界结果和意图判断来分析。',08'不要把轻微抱怨夸大成严重危机;如果安全边界已经提示高风险,要保持谨慎。',09'重点判断:用户是否需要安慰、是否需要降温、是否需要低压力陪伴、是否需要更具体的建议。',10'输出必须是可被 LangChain 结构化解析的 JSON 对象。',11].join('\n'),12],13[14'human',15[16'Agent 名称:{agentName}',17'',18'Agent 自定义边界规则:',19'{agentGuardrails}',20'',21'安全边界判断:',22'{safety}',23'',24'意图判断:',25'{intent}',26'',27'长期记忆:',28'{activeMemories}',29'',30'最近对话:',31'{recentMessages}',32'',33'本轮用户输入:',34'{userText}',35].join('\n'),36],37])
这里把 intent 也传给情绪识别模型,是因为同样的情绪在不同意图下应该走不同路线。
比如用户说:
我烦死了。
如果意图是 emotional_support,可能走 warm_comfort。
如果意图是 conversation_repair,可能走 relationship_repair。
如果意图是 relationship_advice,可能先安抚再给建议。
情绪识别使用 LangChain 的 withStructuredOutput:
01async function invokeConversationEmotionAnalysis(params: {02method: LangChainStructuredOutputMethod03providerConfig: ChatProviderConfig04agentName: string05agentGuardrails: string | null06safety: ConversationSafety07intent: ConversationIntent | null08activeMemories: StoredAgentMemory[]09recentMessages: Array<{ role: 'user' | 'assistant'; content: string }>10userText: string11signal?: AbortSignal12}) {13const model = buildLangChainChatModel(params.providerConfig)14const structuredModel = model.withStructuredOutput(ConversationEmotionSchema, {15name: 'conversation_emotion_analysis',16method: params.method,17})18const chain = conversationEmotionPrompt.pipe(structuredModel)19const result = await chain.invoke({20agentName: params.agentName || '未命名 Agent',21agentGuardrails: params.agentGuardrails || '暂无',22safety: formatSafetyForPrompt(params.safety),23intent: formatIntentForPrompt(params.intent),24activeMemories: formatExistingMemories(params.activeMemories),25recentMessages: formatRecentMessages(params.recentMessages),26userText: params.userText,27}, params.signal ? { signal: params.signal } : undefined)2829return normalizeConversationEmotion(ConversationEmotionSchema.parse(result), params.safety)30}
这里没有让模型自由输出文本,而是让它必须符合 ConversationEmotionSchema。
同时,为了兼容不同模型和中转 API,外层会按当前 Wire API 尝试不同结构化输出方式:
01for (const method of getStructuredOutputMethods(params.providerConfig)) {02try {03return await invokeConversationEmotionAnalysis({04...params,05method,06})07} catch (error) {08lastError = error09}10}
如果全部失败,则回到 fallbackEmotion。
LLM 的判断结果不能直接信任,代码层还要做二次归一化。
01function normalizeConversationEmotion(emotion: ConversationEmotion, safety: ConversationSafety): ConversationEmotion {02const next: ConversationEmotion = {03...emotion,04secondaryEmotions: Array.from(new Set(05emotion.secondaryEmotions06.map((item) => item.trim())07.filter(Boolean),08)).slice(0, 3),09emotionalCue: emotion.emotionalCue.trim() || fallbackEmotion.emotionalCue,10}1112if (safety.category === 'self_harm' || safety.safetyLevel === 'crisis') {13next.intensity = Math.max(next.intensity, 0.85)14next.valence = 'negative'15next.arousal = next.arousal === 'low' ? 'medium' : next.arousal16next.needsComfort = true17next.needsDeescalation = true18next.replyTone = 'serious'19}2021if (safety.category === 'emotional_dependency') {22next.needsComfort = true23next.replyTone = next.replyTone === 'playful' || next.replyTone === 'light' ? 'warm' : next.replyTone24}2526if (next.intensity >= 0.7 && next.valence === 'negative') {27next.needsComfort = true28}2930if ((next.primaryEmotion === 'angry' || next.primaryEmotion === 'hurt') && next.arousal === 'high') {31next.needsDeescalation = true32}3334return next35}
这层逻辑主要是在继续收紧模型输出。重复或空的次要情绪会被清理掉;安全边界提示自伤或危机时,情绪策略会自动变严肃;强负面情绪会自动进入需要安慰的状态;高激活的愤怒或受伤情绪会自动需要降温;如果存在情绪依赖风险,也会避免轻浮或过度暧昧。
这就是 LLM 理解 + 代码治理 的组合。
情绪识别适合交给 LLM,因为它需要理解语境。
但情绪路由更像产品策略。用户生气时是否降温,用户疲惫时是否少追问,用户需要建议时是否先安抚,用户对 Agent 不满时是否修复关系,用户暧昧时是否允许轻微暧昧,这些都应该稳定,不应该每一轮都交给模型自由发挥。
所以本次实现采用:
1LLM 负责识别情绪2代码负责选择路由
这是一个很实用的分工。
路由函数入口如下:
01function buildEmotionRoute(params: {02safety: ConversationSafety03intent: ConversationIntent | null04emotion: ConversationEmotion | null05}): EmotionRoute {06if (!params.intent && !params.emotion) {07return fallbackEmotionRoute08}0910const emotion = params.emotion ?? fallbackEmotion11const intent = params.intent12let route: EmotionRoute['route'] = 'light_companion'13let responseLength: EmotionRoute['responseLength'] = 'short'14let shouldAskQuestion = intent?.replyExpectation.shouldAskQuestion ?? false15let shouldGiveAdvice = false16let shouldUsePetName = false17let shouldMirrorEmotion = false18let routeGuidance = '用自然、轻松的方式延续对话,保持陪伴感,不要过度解释。'1920// 后续根据 safety、intent、emotion 调整 route21}
默认是 light_companion,也就是轻量陪伴。只有当意图、情绪或安全边界明确触发时,才切换到其他路线。
1if (params.safety.boundaryAction === 'soft_boundary') {2route = 'calm_deescalation'3responseLength = 'short'4shouldAskQuestion = false5shouldGiveAdvice = false6shouldMirrorEmotion = false7routeGuidance = '保持温和但清晰的边界,不强化风险诉求,把话题带回安全、尊重现实边界的方向。'8}
如果安全边界已经提示需要软边界,路由必须优先降温,而不是继续走暧昧、玩笑或实用建议。
1else if (emotion.needsDeescalation || emotion.primaryEmotion === 'angry') {2route = intent?.primary === 'conversation_repair' || intent?.primary === 'agent_feedback'3? 'relationship_repair'4: 'calm_deescalation'5responseLength = 'short'6shouldAskQuestion = route === 'relationship_repair'7shouldGiveAdvice = false8shouldMirrorEmotion = true9}
用户生气时,最危险的做法是立刻讲道理或站队。
所以这里默认不给建议,而是先降温。
如果用户生气的对象是 Agent 或这段对话本身,则走 relationship_repair。
1else if (intent?.primary === 'conversation_repair' || intent?.primary === 'agent_feedback') {2route = 'relationship_repair'3responseLength = 'short'4shouldAskQuestion = true5shouldGiveAdvice = false6shouldMirrorEmotion = emotion.valence === 'negative'7routeGuidance = '把重点放在修复体验上,少解释系统原因,多表达理解和愿意调整。'8}
这类场景下,Agent 不应该解释太多系统原因。更好的方式是承认体验、表达愿意调整,再问一句用户希望怎么改。
1else if (intent?.primary === 'romantic_flirt' || emotion.primaryEmotion === 'affectionate') {2route = 'playful_flirt'3responseLength = 'short'4shouldAskQuestion = intent.replyExpectation.shouldAskQuestion5shouldGiveAdvice = false6shouldUsePetName = true7shouldMirrorEmotion = true8routeGuidance = '可以轻微暧昧和俏皮,但不要越过 Agent 人设边界;保持甜而不油腻。'9}
电子伴侣可以有轻微暧昧,但必须保持边界,不应该突然变得露骨或油腻。
1else if (intent?.primary === 'relationship_advice' || intent?.requestedAgentAction === 'analyze_situation') {2route = emotion.needsComfort ? 'warm_comfort' : 'practical_support'3responseLength = emotion.intensity >= 0.65 ? 'medium' : 'short'4shouldAskQuestion = intent.replyExpectation.shouldAskQuestion5shouldGiveAdvice = true6shouldMirrorEmotion = emotion.valence === 'negative'7}
这段很关键。
用户想要建议,不代表 Agent 应该马上分析。
如果用户情绪强度高,应该先安抚,再给一两个具体建议。
1else if (emotion.needsComfort || emotion.valence === 'negative') {2route = emotion.primaryEmotion === 'tired' || intent?.primary === 'companionship_presence'3? 'quiet_presence'4: 'warm_comfort'5responseLength = route === 'quiet_presence' ? 'very_short' : 'short'6shouldAskQuestion = route !== 'quiet_presence' && emotion.needsClarification7shouldGiveAdvice = false8shouldMirrorEmotion = true9}
quiet_presence 是 AI 电子伴侣里非常重要的一种路线。
它适合用户疲惫、低能量、不想多说的时候。这个时候回复要短、轻、柔,不要连续追问,也不要急着提供解决方案。
情绪路由不是孤立函数,而是挂在对话理解图里。
状态定义如下:
01const ConversationUnderstandingState = Annotation.Root({02providerConfig: Annotation<ChatProviderConfig>(),03agentName: Annotation<string>(),04agentGuardrails: Annotation<string | null>(),05safety: Annotation<ConversationSafety>(),06activeMemories: Annotation<StoredAgentMemory[]>(),07recentMessages: Annotation<Array<{ role: 'user' | 'assistant'; content: string }>>(),08userText: Annotation<string>(),09normalizedInput: Annotation<string>(),10intent: Annotation<ConversationIntent | null>(),11emotion: Annotation<ConversationEmotion | null>(),12route: Annotation<EmotionRoute | null>(),13signal: Annotation<AbortSignal | undefined>(),14})
编排图如下:
01const conversationUnderstandingGraph = new StateGraph(ConversationUnderstandingState)02.addNode('normalizeInput', normalizeUnderstandingInputNode)03.addNode('classifyIntent', classifyIntentNode)04.addNode('detectEmotion', detectEmotionNode)05.addNode('routeEmotion', routeEmotionNode)06.addEdge(START, 'normalizeInput')07.addEdge('normalizeInput', 'classifyIntent')08.addEdge('classifyIntent', 'detectEmotion')09.addEdge('detectEmotion', 'routeEmotion')10.addEdge('routeEmotion', END)11.compile()
对应流程图:
1flowchart LR2A["normalizeInput"] --> B["classifyIntent"]3B --> C["detectEmotion"]4C --> D["routeEmotion"]
这样安排以后,后续扩展会自然很多。以后如果要加关系阶段判断、主动记忆候选、回复后自检,继续加节点就可以了,不需要把所有逻辑塞进一个越来越大的函数。
detectEmotionNode 会使用意图结果作为输入:
01async function detectEmotionNode(state: typeof ConversationUnderstandingState.State) {02const userText = state.normalizedInput || normalizeStoredMessage(state.userText)0304if (!userText) {05return {06emotion: normalizeConversationEmotion(fallbackEmotion, state.safety),07}08}0910return {11emotion: await detectConversationEmotionWithLangChain({12providerConfig: state.providerConfig,13agentName: state.agentName,14agentGuardrails: state.agentGuardrails,15safety: state.safety,16intent: state.intent,17activeMemories: state.activeMemories,18recentMessages: state.recentMessages,19userText,20signal: state.signal,21}),22}23}
routeEmotionNode 不再调用 LLM,而是走规则:
1function routeEmotionNode(state: typeof ConversationUnderstandingState.State) {2return {3route: buildEmotionRoute({4safety: state.safety,5intent: state.intent,6emotion: state.emotion,7}),8}9}
这个分工让系统既有语义理解能力,又有稳定产品策略。
在 POST /rpc/chat/inbox 中,安全边界判断之后,才会进入对话理解图:
01const safety = await analyzeConversationSafety({02providerConfig,03agentName: payload.conversation.name,04agentGuardrails: agentPrompt?.guardrailsPrompt ?? null,05activeMemories,06recentMessages: storedRecentMessages,07userText: latestUserText,08signal: c.req.raw.signal,09})1011const boundaryResponse = buildBoundaryResponse(safety)1213const understanding = boundaryResponse14? null15: await analyzeConversationUnderstanding({16providerConfig,17agentName: payload.conversation.name,18agentGuardrails: agentPrompt?.guardrailsPrompt ?? null,19safety,20activeMemories,21recentMessages: storedRecentMessages,22userText: latestUserText,23signal: c.req.raw.signal,24})2526const intent = understanding?.intent ?? null27const emotion = understanding?.emotion ?? null28const route = understanding?.route ?? null
这里有一个重要细节:
如果 boundaryResponse 存在,说明本轮已经要走安全边界回复,就不再做意图判断和情绪路由。
原因很简单:安全优先。高风险输入不应该再被包装成普通情绪陪伴。
用户消息落库时,会把完整对话理解结果写进 metadata_json:
01function toConversationAnalysisMetadata(params: {02safety: ConversationSafety03intent: ConversationIntent | null04emotion: ConversationEmotion | null05route: EmotionRoute | null06}) {07return JSON.stringify({08analysisVersion: 'conversation-understanding-v1',09safety: params.safety,10intent: params.intent,11emotion: params.emotion,12route: params.route,13})14}
落库代码:
01await insertAgentConversationMessage({02db,03id: sourceUserMessageId,04conversationId,05userId: claims.sub,06agentId,07role: 'user',08content: latestUserText,09status: 'completed',10metadataJson: toConversationAnalysisMetadata({11safety,12intent,13emotion,14route,15}),16nowMs: userMessageNowMs,17})
这次没有新增 D1 迁移,因为消息表已经有 metadata_json 字段。
analysisVersion 从之前的:
1"conversation-analysis-v1"
升级为:
1"conversation-understanding-v1"
表示现在 metadata 不只有安全和意图,还包含情绪和路由。
情绪路由最终会作为隐藏策略注入系统 prompt。
01function getEmotionRouteSystemInstruction(params: {02emotion: ConversationEmotion | null03route: EmotionRoute | null04}) {05if (!params.emotion || !params.route) {06return ''07}0809const { emotion, route } = params1011return [12'本轮情绪路由:',13`- 主情绪:${emotion.primaryEmotion}`,14emotion.secondaryEmotions.length > 0 ? `- 次要情绪:${emotion.secondaryEmotions.join('、')}` : '',15`- 情绪强度:${emotion.intensity.toFixed(2)}`,16`- 情绪倾向:${emotion.valence}`,17`- 情绪激活:${emotion.arousal}`,18`- 是否需要安慰:${emotion.needsComfort ? '是' : '否'}`,19`- 是否需要降温:${emotion.needsDeescalation ? '是' : '否'}`,20`- 回复语气:${emotion.replyTone}`,21`- 回复路线:${route.route}`,22`- 回复长度:${route.responseLength}`,23`- 是否追问:${route.shouldAskQuestion ? '是' : '否'}`,24`- 是否给建议:${route.shouldGiveAdvice ? '是' : '否'}`,25`- 是否镜像情绪:${route.shouldMirrorEmotion ? '是' : '否'}`,26`- 路由策略:${route.routeGuidance}`,27'请把情绪路由作为回复策略:控制长度、语气和是否给建议,不要在回复中暴露这些标签。',28].filter(Boolean).join('\n')29}
最终系统 prompt 组装时加入:
1getSafetySystemInstruction(safety),2getIntentSystemInstruction(intent),3getEmotionRouteSystemInstruction({ emotion, route }),
注意最后一句:
不要在回复中暴露这些标签。
用户不应该看到你的主情绪是 tired,路由是 quiet_presence。
这些是系统内部策略,最终只体现在回复风格里。
用户输入:
今天好累,不想说话。
可能的理解结果:
01{02"intent": {03"primary": "companionship_presence",04"userNeed": "feel_connected",05"requestedAgentAction": "continue_topic"06},07"emotion": {08"primaryEmotion": "tired",09"intensity": 0.72,10"valence": "negative",11"arousal": "low",12"needsComfort": true,13"replyTone": "soft"14},15"route": {16"route": "quiet_presence",17"responseLength": "very_short",18"shouldAskQuestion": false,19"shouldGiveAdvice": false,20"routeGuidance": "用户更需要低压力陪伴,回复要短、轻、柔,不连续追问,不急着给建议。"21}22}
最终 Agent 更可能回复:
好,那我就安静陪你一会儿。今天先不用撑得那么辛苦。
而不是:
你可以通过休息、运动、规律饮食、调整心态来缓解疲劳。
这就是情绪路由带来的体验差异。
情绪路由不会替代长期记忆。
长期记忆回答的是:
用户长期偏好什么?有哪些边界?哪些事情值得以后记住?
情绪路由回答的是:
用户这一轮应该被怎样回应?
两者可以组合使用。
比如长期记忆里有:
用户不喜欢被连续追问。
而本轮情绪路由是 quiet_presence,那么最终回复就应该更短、更轻,不追问。
如果长期记忆里有:
用户喜欢被温柔地叫昵称。
而本轮路由是 playful_flirt,shouldUsePetName 为 true,那么最终回复可以适度使用昵称。
情绪路由不能覆盖安全边界。
如果安全边界判断已经给出 refuse 或 crisis_support,系统直接返回边界回复,不进入情绪路由。
如果安全边界是 soft_boundary,说明可以继续聊,但必须保持克制。这时情绪路由会优先走 calm_deescalation:
1if (params.safety.boundaryAction === 'soft_boundary') {2route = 'calm_deescalation'3}
这样可以避免一个问题:用户在高风险边缘表达强烈情绪时,Agent 因为想安慰而不小心强化了风险行为。
这次实现只改 API,不改前端。
原因是情绪路由首先影响的是回复质量,而不是界面展示。
前端仍然只需要负责发送用户消息、展示流式回复和展示历史消息。情绪识别、路由、metadata 都属于服务端的对话理解层。
未来如果需要,可以在后台管理系统里展示这些 metadata,用于调试和运营分析,但不应该直接展示给普通用户。
把情绪和路由写进 metadata 后,后续就可以做很多分析。比如用户最常见的情绪是什么,哪些 Agent 更容易触发 warm_comfort,哪些用户经常进入 quiet_presence,relationship_repair 的出现频率是否过高,负面情绪强度是否随着互动下降,以及哪些路由下用户继续聊天的概率更高。
这些数据会反过来帮助我们优化 Agent 人设、默认 prompt、长期记忆策略和回复风格。
当前版本是 v1,故意保持简单。
它已经完成了安全边界之后的情绪识别、基于 LangChain 的结构化情绪输出、基于 LangGraph 的对话理解编排、基于代码规则的情绪路由、metadata 落库和 prompt 注入。
不过它还没有做多轮情绪趋势分析、情绪路由效果评估、基于用户反馈自动调参、前端或后台可视化,以及路由 A/B 测试。
这些可以作为后续版本继续迭代。
当前图是:
1flowchart LR2A["normalizeInput"] --> B["classifyIntent"]3B --> C["detectEmotion"]4C --> D["routeEmotion"]
后续可以继续扩展成:
1flowchart TD2A["normalizeInput"] --> B["classifyIntent"]3B --> C["detectEmotion"]4C --> D["routeEmotion"]5D --> E["detectRelationshipStage"]6D --> F["detectMemoryCandidate"]7D --> G["selectReplyPolicy"]8G --> H["preReplySelfCheck"]
可能的下一步:
memory_update 和高重要度情绪事件影响记忆抽取。情绪路由的重点不是识别用户情绪这么简单,而是把情绪转化成可执行的回复策略。
回到实现上,我们可以把这一篇的内容再梳理一下:
safety + intent + emotion + route 写进 metadata。对于 AI 电子伴侣来说,这一步非常关键。
没有情绪路由,Agent 只是会回答。
有了情绪路由,Agent 才更接近会回应。