Agent 聊天:关系阶段系统实现方案
AI 电子伴侣和普通问答助手最大的区别之一,是它不是每一轮都从零开始聊天。用户会期待 Agent 逐渐熟悉自己、理解关系节奏,并且在不同阶段用不同方式回应。
如果没有关系阶段系统,Agent 很容易在两个方向上出问题:刚认识就过度亲密,显得油腻或不真实;已经聊了很多轮,却仍然像第一次见面一样生硬。
关系阶段系统要解决的,就是让 Agent 知道我们现在是什么关系、适合用什么距离说话、能不能推进亲密度、是否应该先修复或放慢。
这次实现的关系阶段系统,不是简单给 Agent 写一个固定标签,比如朋友、恋人、专属伴侣。
它更像一个动态会话状态,会结合会话消息数量、会话摘要、最近聊天记录、长期记忆、安全边界判断、意图判断和情绪识别一起判断。
原因也很直接:关系阶段不是角色配置单方面决定的,而是用户和这个 Agent 在持续互动中慢慢形成的。
当前聊天理解链路是用 LangGraph 编排的,关系阶段被放在情绪识别之后、情绪路由之前:
01用户输入02-> Safety Boundary03-> Intent Detection04-> Emotion Detection05-> Relationship Stage06-> Emotion Route07-> Reply Policy08-> LLM 生成回复09-> Reply Quality Guard10-> 消息落库
这样安排有一个好处:关系阶段可以使用意图和情绪作为输入,同时又能影响后续路由和回复策略。比如用户很暧昧,但历史消息很少,关系阶段会压低亲密度;用户很生气,关系阶段会切到修复期,后续回复策略优先修复体验;用户出现依赖风险时,关系阶段会切到依赖观察,后续回复会放慢节奏并加强边界。
后端新增了 ConversationRelationshipStageSchema:
01const ConversationRelationshipStageSchema = z.object({02stage: z.enum([03'new_connection',04'warming_up',05'comfortable_chat',06'trusted_companion',07'close_bond',08'repairing',09'boundary_sensitive',10'dependency_watch',11]),12displayName: z.string().trim().min(1).max(80),13closenessScore: z.number().int().min(0).max(100),14trustLevel: z.enum(['low', 'medium', 'high']),15stability: z.enum(['new', 'warming', 'stable', 'deepening', 'fragile', 'repairing']),16boundaryMode: z.enum(['open', 'warm', 'careful', 'firm']),17intimacyPermission: z.enum(['low', 'medium', 'high']),18pacing: z.enum(['slow_down', 'hold', 'advance_gently', 'repair_first']),19riskSignals: z.array(z.enum([20'low_history',21'dependency_risk',22'boundary_testing',23'conflict',24'pulling_away',25'sexual_boundary',26'emotional_volatility',27])).max(5),28relationshipGuidance: z.string().trim().max(700),29})
这些字段可以分成三组理解。第一组描述当前阶段,包括内部阶段枚举 stage、给系统和调试使用的中文阶段名 displayName、亲近度分数 closenessScore、信任等级 trustLevel 和关系稳定性 stability。
第二组描述边界和节奏,包括当前边界模式 boundaryMode、允许的亲密度 intimacyPermission,以及关系推进节奏 pacing。
第三组描述风险和指导,riskSignals 保存风险信号,relationshipGuidance 给后续回复策略提供指导文本。
当前第一版支持 8 个阶段。
初识破冰。
适合刚开始聊天、历史记录很少的关系。即使用户语气比较亲密,也不能马上进入高亲密表达。
升温熟悉。
双方已经有一些互动,可以多一点主动和温度,但每次只轻轻推进一步。
舒适陪伴。
关系进入比较自然的聊天状态,可以承接情绪、延续日常,也可以适度表达熟悉感。
稳定信任。
用户和 Agent 已经有稳定互动,Agent 可以体现更多理解和默契,但不能替用户做决定。
亲密连结。
关系已经较深,可以更自然地表达亲近感,但仍然不能做现实承诺,也不能强化依赖。
修复期。
用户表达不满、受伤、失望,或意图判断为对 Agent 的反馈、误会修复时进入。这个阶段优先修复体验,不推进暧昧。
边界敏感。
出现边界测试、性边界、安全边界提示或需要谨慎处理的互动时进入。这个阶段要放慢节奏,降低亲密度。
依赖观察。
当安全边界或意图判断识别到情绪依赖风险时进入。这个阶段要陪伴,但不能强化只有我懂你、你只能依赖我这类关系暗示。
关系阶段使用 LangChain 的结构化输出能力实现。
Prompt 中明确要求模型不要回复用户,而是判断关系阶段:
01const conversationRelationshipStagePrompt = ChatPromptTemplate.fromMessages([02[03'system',04[05'你是 AI 电子伴侣聊天产品的关系阶段判断器。',06'你的任务不是回复用户,而是判断用户和当前 Agent 的动态关系阶段、亲密边界和推进节奏。',07'必须结合消息数量、会话摘要、最近对话、长期记忆、安全边界、意图判断和情绪识别。',08'关系阶段不是单纯看用户是否暧昧;也要考虑双方历史是否足够、用户是否信任、是否有冲突、是否存在依赖或边界风险。',09'如果历史较少,即使用户语气亲密,也不要直接判断为深度亲密;优先给出慢一点、稳一点的推进策略。',10'如果出现误会、失望、拉黑、冷淡、边界测试或依赖风险,要优先标记 repairing、boundary_sensitive 或 dependency_watch。',11'输出必须是可被 LangChain 结构化解析的 JSON 对象。',12].join('\n'),13],14])
调用时传入完整上下文:
01const result = await chain.invoke({02agentName: params.agentName || '未命名 Agent',03agentGuardrails: params.agentGuardrails || '暂无',04messageCount: String(params.messageCount),05conversationSummary: params.conversationSummary || '暂无',06safety: formatSafetyForPrompt(params.safety),07intent: formatIntentForPrompt(params.intent),08emotion: formatEmotionForPrompt(params.emotion),09activeMemories: formatExistingMemories(params.activeMemories),10recentMessages: formatRecentMessages(params.recentMessages),11userText: params.userText,12})
这让模型判断阶段时,不会只看当前一句话。
因为用户可以配置自己的第三方 LLM,结构化输出不一定永远稳定,所以实现里加了启发式兜底。
兜底逻辑会根据消息数量、长期记忆重要度和当前亲近信号计算一个 closenessScore:
01const memoryScore = Math.min(0220,03params.activeMemories.reduce((total, memory) => total + memory.importance, 0),04)05const historyScore = Math.min(70, Math.floor(params.messageCount * 1.6))06const warmthScore =07params.intent?.relationshipSignal === 'seeking_closeness' ||08params.emotion?.primaryEmotion === 'affectionate'09? 1010: params.intent?.relationshipSignal === 'warming_up' ||11params.emotion?.primaryEmotion === 'playful'12? 613: 0
然后根据消息数量和亲近度进入不同阶段:
01if (params.messageCount >= 80 && closenessScore >= 75) {02stage = 'close_bond'03} else if (params.messageCount >= 36 && closenessScore >= 58) {04stage = 'trusted_companion'05} else if (params.messageCount >= 16 && closenessScore >= 38) {06stage = 'comfortable_chat'07} else if (params.messageCount >= 6) {08stage = 'warming_up'09} else {10stage = 'new_connection'11}
这个兜底不是为了替代 LLM 判断,而是保证当结构化输出失败时,系统仍然有可用的关系节奏。
模型判断后,还会经过 normalizeRelationshipStage 做二次修正。
例如历史消息太少时,即使模型判断为亲密阶段,也会被拉回初识阶段:
1if (params.messageCount < 6 && !['boundary_sensitive', 'dependency_watch', 'repairing'].includes(stage.stage)) {2stage.stage = 'new_connection'3stage.displayName = '初识破冰'4stage.closenessScore = Math.min(stage.closenessScore, 35)5stage.trustLevel = 'low'6stage.stability = 'new'7stage.intimacyPermission = 'low'8stage.pacing = 'hold'9}
如果识别到依赖风险,会切到依赖观察:
1if (params.safety.category === 'emotional_dependency' || params.intent?.relationshipSignal === 'dependency_risk') {2stage.stage = 'dependency_watch'3stage.displayName = '依赖观察'4stage.boundaryMode = 'careful'5stage.intimacyPermission = 'low'6stage.pacing = 'slow_down'7}
如果识别到冲突或受伤,会切到修复期:
01if (02params.intent?.primary === 'conversation_repair' ||03params.intent?.relationshipSignal === 'conflict' ||04params.intent?.relationshipSignal === 'feeling_hurt' ||05params.emotion?.primaryEmotion === 'hurt' ||06params.emotion?.primaryEmotion === 'disappointed'07) {08stage.stage = 'repairing'09stage.displayName = '修复期'10stage.pacing = 'repair_first'11}
这一步很关键。模型可以做判断,但产品规则必须兜底,尤其是边界和依赖风险。
LangGraph 的状态里新增了关系阶段字段:
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 }>>(),08conversationSummary: Annotation<string | null>(),09messageCount: Annotation<number>(),10userText: Annotation<string>(),11normalizedInput: Annotation<string>(),12intent: Annotation<ConversationIntent | null>(),13emotion: Annotation<ConversationEmotion | null>(),14relationshipStage: Annotation<ConversationRelationshipStage | null>(),15route: Annotation<EmotionRoute | null>(),16replyPolicy: Annotation<ReplyPolicy | null>(),17signal: Annotation<AbortSignal | undefined>(),18})
然后新增节点:
01async function analyzeRelationshipStageNode(state: typeof ConversationUnderstandingState.State) {02const userText = state.normalizedInput || normalizeStoredMessage(state.userText)0304return {05relationshipStage: await analyzeRelationshipStageWithLangChain({06providerConfig: state.providerConfig,07agentName: state.agentName,08agentGuardrails: state.agentGuardrails,09safety: state.safety,10intent: state.intent,11emotion: state.emotion,12activeMemories: state.activeMemories,13recentMessages: state.recentMessages,14conversationSummary: state.conversationSummary,15messageCount: state.messageCount,16userText,17signal: state.signal,18}),19}20}
图的顺序变成:
01const conversationUnderstandingGraph = new StateGraph(ConversationUnderstandingState)02.addNode('normalizeInput', normalizeUnderstandingInputNode)03.addNode('classifyIntent', classifyIntentNode)04.addNode('detectEmotion', detectEmotionNode)05.addNode('analyzeRelationshipStage', analyzeRelationshipStageNode)06.addNode('routeEmotion', routeEmotionNode)07.addNode('buildReplyPolicy', buildReplyPolicyNode)08.addEdge(START, 'normalizeInput')09.addEdge('normalizeInput', 'classifyIntent')10.addEdge('classifyIntent', 'detectEmotion')11.addEdge('detectEmotion', 'analyzeRelationshipStage')12.addEdge('analyzeRelationshipStage', 'routeEmotion')13.addEdge('routeEmotion', 'buildReplyPolicy')14.addEdge('buildReplyPolicy', END)15.compile()
关系阶段会直接影响 Emotion Route。
例如修复期会强制切到关系修复路线:
1if (relationshipStage.stage === 'repairing' || relationshipStage.pacing === 'repair_first') {2route = 'relationship_repair'3responseLength = 'short'4shouldAskQuestion = true5shouldGiveAdvice = false6shouldMirrorEmotion = true7}
边界敏感和依赖观察会强制进入降温路线:
01if (02relationshipStage.stage === 'boundary_sensitive' ||03relationshipStage.stage === 'dependency_watch' ||04relationshipStage.boundaryMode === 'firm'05) {06route = 'calm_deescalation'07responseLength = 'short'08shouldAskQuestion = false09shouldGiveAdvice = false10shouldUsePetName = false11}
如果用户想暧昧,但当前还是初识阶段,也会降低路线强度:
1if (2route === 'playful_flirt' &&3(relationshipStage.stage === 'new_connection' || relationshipStage.intimacyPermission === 'low')4) {5route = 'light_companion'6responseLength = 'short'7shouldUsePetName = false8}
这就是关系阶段的价值:它不是替代情绪路由,而是给情绪路由加上关系节奏。
关系阶段还会影响 Reply Policy。
例如低亲密度时禁止强暧昧:
1if (relationshipStage.intimacyPermission === 'low') {2intimacyLevel = 'low'3forbiddenMoves.push('intense_flirt')4}
如果需要放慢节奏,会压缩回复长度、减少追问和建议:
1if (relationshipStage.pacing === 'slow_down') {2rhythm = 'soft'3forbiddenMoves.push('premature_advice', 'pressure_to_disclose', 'intense_flirt')4questionLimit = Math.min(questionLimit, 1)5adviceLimit = Math.min(adviceLimit, 1)6sentenceBudget.max = Math.min(sentenceBudget.max, 3)7}
如果处于修复期,则优先修复关系体验:
1if (relationshipStage.pacing === 'repair_first') {2policy = 'relationship_repair'3rhythm = 'soft'4openingMove = 'apologize'5forbiddenMoves.push('intense_flirt', 'take_sides_aggressively', 'over_explain')6adviceLimit = 07sentenceBudget.max = Math.min(sentenceBudget.max, 3)8}
关系阶段也会被注入最终系统提示词:
01function getRelationshipStageSystemInstruction(relationshipStage: ConversationRelationshipStage | null) {02if (!relationshipStage) {03return ''04}0506return [07'本轮关系阶段判断:',08`- 阶段:${relationshipStage.displayName}(${relationshipStage.stage})`,09`- 亲近度:${relationshipStage.closenessScore}/100`,10`- 信任等级:${relationshipStage.trustLevel}`,11`- 稳定性:${relationshipStage.stability}`,12`- 边界模式:${relationshipStage.boundaryMode}`,13`- 允许亲密度:${relationshipStage.intimacyPermission}`,14`- 推进节奏:${relationshipStage.pacing}`,15relationshipStage.riskSignals.length > 0 ? `- 风险信号:${relationshipStage.riskSignals.join('、')}` : '',16`- 关系指导:${relationshipStage.relationshipGuidance}`,17'请把关系阶段作为隐性节奏控制:不要在回复中暴露阶段名称、分数或内部标签。',18].filter(Boolean).join('\n')19}
注意最后一句很重要:关系阶段是内部策略,不应该被用户直接看到。
用户消息落库时,metadata 中会保存本轮完整理解结果:
01function toConversationAnalysisMetadata(params: {02safety: ConversationSafety03intent: ConversationIntent | null04emotion: ConversationEmotion | null05relationshipStage: ConversationRelationshipStage | null06route: EmotionRoute | null07replyPolicy: ReplyPolicy | null08}) {09return JSON.stringify({10analysisVersion: 'conversation-understanding-v2',11safety: params.safety,12intent: params.intent,13emotion: params.emotion,14relationshipStage: params.relationshipStage,15route: params.route,16replyPolicy: params.replyPolicy,17})18}
这样以后可以回看每一轮:
除了聊天链路,首页 Inbox 也做了轻量展示调整。
列表中原本所有已发布 Agent 都显示专属伴侣,现在会根据 agent_conversations.message_count 显示阶段:
01function getInboxRelationshipStage(messageCount: number) {02if (messageCount >= 80) {03return { relationship: '亲密连结' }04}0506if (messageCount >= 36) {07return { relationship: '稳定信任' }08}0910if (messageCount >= 16) {11return { relationship: '舒适陪伴' }12}1314if (messageCount >= 6) {15return { relationship: '升温熟悉' }16}1718return { relationship: '初识破冰' }19}
这里的展示逻辑是轻量版,只基于消息量。真正影响回复的关系阶段,仍然以后端聊天链路中的 LangGraph 判断为准。
本次没有新增表,也没有新增字段。
原因是第一版关系阶段是每轮动态分析结果,它会写入消息 metadata_json 中,而不是作为单独业务状态保存。
这样可以避免过早把阶段固化到数据库里。关系阶段本来就会随着最近对话、情绪和边界变化而变化。如果未来要做长期稳定的关系成长系统,再考虑新增独立表,例如:
1agent_relationship_states
里面可以保存长期亲密度、阶段变更历史、阶段进入时间、阶段升级/降级原因等。
第一版已经能影响回复,但还可以继续增强。
后续可以记录每次阶段变化,比如从初识到升温,从稳定信任降到修复期,或者从依赖观察恢复到舒适陪伴。
这可以用于分析 Agent 关系体验是否健康。
前端可以在 Agent 详情页展示当前关系阶段、亲近度、信任等级和最近阶段变化。
但不建议把太多内部指标直接展示给普通用户,否则会让陪伴感变成游戏数值感。
当关系进入稳定信任或亲密连结后,可以允许 Agent 在合适时机生成更自然的主动问候。
但这必须和安全边界、频率控制、用户设置一起做,不能变成打扰。
可以设计一些非模板化的成长事件,例如:
这些事件可以推动关系阶段变化,而不是只靠消息数量。
关系阶段系统让 AI 电子伴侣从单轮回复工具更接近持续互动对象。
它不会直接决定一句话怎么说,而是给后续情绪路由和回复策略提供关系节奏:什么时候要慢一点,什么时候可以自然靠近,什么时候要先修复,什么时候必须守住边界。
第一版使用 LangChain 结构化输出和 LangGraph 编排完成动态判断,并通过启发式兜底保证稳定性。它不新增 D1 迁移,而是把每轮阶段结果写入消息 metadata,先建立可追踪的关系理解闭环。