创建时间: 2026-06-16最后更新: 2026-06-16

概述

Agent 聊天:关系阶段系统实现方案

AI 电子伴侣和普通问答助手最大的区别之一,是它不是每一轮都从零开始聊天。用户会期待 Agent 逐渐熟悉自己、理解关系节奏,并且在不同阶段用不同方式回应。

如果没有关系阶段系统,Agent 很容易在两个方向上出问题:刚认识就过度亲密,显得油腻或不真实;已经聊了很多轮,却仍然像第一次见面一样生硬。

关系阶段系统要解决的,就是让 Agent 知道我们现在是什么关系、适合用什么距离说话、能不能推进亲密度、是否应该先修复或放慢

设计目标

这次实现的关系阶段系统,不是简单给 Agent 写一个固定标签,比如朋友恋人专属伴侣

它更像一个动态会话状态,会结合会话消息数量、会话摘要、最近聊天记录、长期记忆、安全边界判断、意图判断和情绪识别一起判断。

原因也很直接:关系阶段不是角色配置单方面决定的,而是用户和这个 Agent 在持续互动中慢慢形成的。

完整链路

当前聊天理解链路是用 LangGraph 编排的,关系阶段被放在情绪识别之后、情绪路由之前:

index.txt
01
用户输入
02
-> Safety Boundary
03
-> Intent Detection
04
-> Emotion Detection
05
-> Relationship Stage
06
-> Emotion Route
07
-> Reply Policy
08
-> LLM 生成回复
09
-> Reply Quality Guard
10
-> 消息落库

这样安排有一个好处:关系阶段可以使用意图和情绪作为输入,同时又能影响后续路由和回复策略。比如用户很暧昧,但历史消息很少,关系阶段会压低亲密度;用户很生气,关系阶段会切到修复期,后续回复策略优先修复体验;用户出现依赖风险时,关系阶段会切到依赖观察,后续回复会放慢节奏并加强边界。

关系阶段数据结构

后端新增了 ConversationRelationshipStageSchema

index.ts
01
const ConversationRelationshipStageSchema = z.object({
02
stage: 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
]),
12
displayName: z.string().trim().min(1).max(80),
13
closenessScore: z.number().int().min(0).max(100),
14
trustLevel: z.enum(['low', 'medium', 'high']),
15
stability: z.enum(['new', 'warming', 'stable', 'deepening', 'fragile', 'repairing']),
16
boundaryMode: z.enum(['open', 'warm', 'careful', 'firm']),
17
intimacyPermission: z.enum(['low', 'medium', 'high']),
18
pacing: z.enum(['slow_down', 'hold', 'advance_gently', 'repair_first']),
19
riskSignals: 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),
28
relationshipGuidance: z.string().trim().max(700),
29
})

这些字段可以分成三组理解。第一组描述当前阶段,包括内部阶段枚举 stage、给系统和调试使用的中文阶段名 displayName、亲近度分数 closenessScore、信任等级 trustLevel 和关系稳定性 stability

第二组描述边界和节奏,包括当前边界模式 boundaryMode、允许的亲密度 intimacyPermission,以及关系推进节奏 pacing

第三组描述风险和指导riskSignals 保存风险信号,relationshipGuidance 给后续回复策略提供指导文本。

阶段含义

当前第一版支持 8 个阶段。

new_connection

初识破冰。

适合刚开始聊天、历史记录很少的关系。即使用户语气比较亲密,也不能马上进入高亲密表达。

warming_up

升温熟悉。

双方已经有一些互动,可以多一点主动和温度,但每次只轻轻推进一步。

comfortable_chat

舒适陪伴。

关系进入比较自然的聊天状态,可以承接情绪、延续日常,也可以适度表达熟悉感。

trusted_companion

稳定信任。

用户和 Agent 已经有稳定互动,Agent 可以体现更多理解和默契,但不能替用户做决定。

close_bond

亲密连结。

关系已经较深,可以更自然地表达亲近感,但仍然不能做现实承诺,也不能强化依赖。

repairing

修复期。

用户表达不满、受伤、失望,或意图判断为对 Agent 的反馈、误会修复时进入。这个阶段优先修复体验,不推进暧昧。

boundary_sensitive

边界敏感。

出现边界测试、性边界、安全边界提示或需要谨慎处理的互动时进入。这个阶段要放慢节奏,降低亲密度。

dependency_watch

依赖观察。

当安全边界或意图判断识别到情绪依赖风险时进入。这个阶段要陪伴,但不能强化只有我懂你你只能依赖我这类关系暗示。

LangChain 判断器

关系阶段使用 LangChain 的结构化输出能力实现。

Prompt 中明确要求模型不要回复用户,而是判断关系阶段:

index.ts
01
const 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
])

调用时传入完整上下文:

index.ts
01
const result = await chain.invoke({
02
agentName: params.agentName || '未命名 Agent',
03
agentGuardrails: params.agentGuardrails || '暂无',
04
messageCount: String(params.messageCount),
05
conversationSummary: params.conversationSummary || '暂无',
06
safety: formatSafetyForPrompt(params.safety),
07
intent: formatIntentForPrompt(params.intent),
08
emotion: formatEmotionForPrompt(params.emotion),
09
activeMemories: formatExistingMemories(params.activeMemories),
10
recentMessages: formatRecentMessages(params.recentMessages),
11
userText: params.userText,
12
})

这让模型判断阶段时,不会只看当前一句话。

启发式兜底

因为用户可以配置自己的第三方 LLM,结构化输出不一定永远稳定,所以实现里加了启发式兜底。

兜底逻辑会根据消息数量、长期记忆重要度和当前亲近信号计算一个 closenessScore

index.ts
01
const memoryScore = Math.min(
02
20,
03
params.activeMemories.reduce((total, memory) => total + memory.importance, 0),
04
)
05
const historyScore = Math.min(70, Math.floor(params.messageCount * 1.6))
06
const warmthScore =
07
params.intent?.relationshipSignal === 'seeking_closeness' ||
08
params.emotion?.primaryEmotion === 'affectionate'
09
? 10
10
: params.intent?.relationshipSignal === 'warming_up' ||
11
params.emotion?.primaryEmotion === 'playful'
12
? 6
13
: 0

然后根据消息数量和亲近度进入不同阶段:

index.ts
01
if (params.messageCount >= 80 && closenessScore >= 75) {
02
stage = 'close_bond'
03
} else if (params.messageCount >= 36 && closenessScore >= 58) {
04
stage = 'trusted_companion'
05
} else if (params.messageCount >= 16 && closenessScore >= 38) {
06
stage = 'comfortable_chat'
07
} else if (params.messageCount >= 6) {
08
stage = 'warming_up'
09
} else {
10
stage = 'new_connection'
11
}

这个兜底不是为了替代 LLM 判断,而是保证当结构化输出失败时,系统仍然有可用的关系节奏。

规范化规则

模型判断后,还会经过 normalizeRelationshipStage 做二次修正。

例如历史消息太少时,即使模型判断为亲密阶段,也会被拉回初识阶段:

index.ts
1
if (params.messageCount < 6 && !['boundary_sensitive', 'dependency_watch', 'repairing'].includes(stage.stage)) {
2
stage.stage = 'new_connection'
3
stage.displayName = '初识破冰'
4
stage.closenessScore = Math.min(stage.closenessScore, 35)
5
stage.trustLevel = 'low'
6
stage.stability = 'new'
7
stage.intimacyPermission = 'low'
8
stage.pacing = 'hold'
9
}

如果识别到依赖风险,会切到依赖观察:

index.ts
1
if (params.safety.category === 'emotional_dependency' || params.intent?.relationshipSignal === 'dependency_risk') {
2
stage.stage = 'dependency_watch'
3
stage.displayName = '依赖观察'
4
stage.boundaryMode = 'careful'
5
stage.intimacyPermission = 'low'
6
stage.pacing = 'slow_down'
7
}

如果识别到冲突或受伤,会切到修复期:

index.ts
01
if (
02
params.intent?.primary === 'conversation_repair' ||
03
params.intent?.relationshipSignal === 'conflict' ||
04
params.intent?.relationshipSignal === 'feeling_hurt' ||
05
params.emotion?.primaryEmotion === 'hurt' ||
06
params.emotion?.primaryEmotion === 'disappointed'
07
) {
08
stage.stage = 'repairing'
09
stage.displayName = '修复期'
10
stage.pacing = 'repair_first'
11
}

这一步很关键。模型可以做判断,但产品规则必须兜底,尤其是边界和依赖风险。

接入 LangGraph

LangGraph 的状态里新增了关系阶段字段:

index.ts
01
const ConversationUnderstandingState = Annotation.Root({
02
providerConfig: Annotation<ChatProviderConfig>(),
03
agentName: Annotation<string>(),
04
agentGuardrails: Annotation<string | null>(),
05
safety: Annotation<ConversationSafety>(),
06
activeMemories: Annotation<StoredAgentMemory[]>(),
07
recentMessages: Annotation<Array<{ role: 'user' | 'assistant'; content: string }>>(),
08
conversationSummary: Annotation<string | null>(),
09
messageCount: Annotation<number>(),
10
userText: Annotation<string>(),
11
normalizedInput: Annotation<string>(),
12
intent: Annotation<ConversationIntent | null>(),
13
emotion: Annotation<ConversationEmotion | null>(),
14
relationshipStage: Annotation<ConversationRelationshipStage | null>(),
15
route: Annotation<EmotionRoute | null>(),
16
replyPolicy: Annotation<ReplyPolicy | null>(),
17
signal: Annotation<AbortSignal | undefined>(),
18
})

然后新增节点:

index.ts
01
async function analyzeRelationshipStageNode(state: typeof ConversationUnderstandingState.State) {
02
const userText = state.normalizedInput || normalizeStoredMessage(state.userText)
03
04
return {
05
relationshipStage: await analyzeRelationshipStageWithLangChain({
06
providerConfig: state.providerConfig,
07
agentName: state.agentName,
08
agentGuardrails: state.agentGuardrails,
09
safety: state.safety,
10
intent: state.intent,
11
emotion: state.emotion,
12
activeMemories: state.activeMemories,
13
recentMessages: state.recentMessages,
14
conversationSummary: state.conversationSummary,
15
messageCount: state.messageCount,
16
userText,
17
signal: state.signal,
18
}),
19
}
20
}

图的顺序变成:

index.ts
01
const 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。

例如修复期会强制切到关系修复路线:

index.ts
1
if (relationshipStage.stage === 'repairing' || relationshipStage.pacing === 'repair_first') {
2
route = 'relationship_repair'
3
responseLength = 'short'
4
shouldAskQuestion = true
5
shouldGiveAdvice = false
6
shouldMirrorEmotion = true
7
}

边界敏感和依赖观察会强制进入降温路线:

index.ts
01
if (
02
relationshipStage.stage === 'boundary_sensitive' ||
03
relationshipStage.stage === 'dependency_watch' ||
04
relationshipStage.boundaryMode === 'firm'
05
) {
06
route = 'calm_deescalation'
07
responseLength = 'short'
08
shouldAskQuestion = false
09
shouldGiveAdvice = false
10
shouldUsePetName = false
11
}

如果用户想暧昧,但当前还是初识阶段,也会降低路线强度:

index.ts
1
if (
2
route === 'playful_flirt' &&
3
(relationshipStage.stage === 'new_connection' || relationshipStage.intimacyPermission === 'low')
4
) {
5
route = 'light_companion'
6
responseLength = 'short'
7
shouldUsePetName = false
8
}

这就是关系阶段的价值:它不是替代情绪路由,而是给情绪路由加上关系节奏。

影响回复策略

关系阶段还会影响 Reply Policy。

例如低亲密度时禁止强暧昧:

index.ts
1
if (relationshipStage.intimacyPermission === 'low') {
2
intimacyLevel = 'low'
3
forbiddenMoves.push('intense_flirt')
4
}

如果需要放慢节奏,会压缩回复长度、减少追问和建议:

index.ts
1
if (relationshipStage.pacing === 'slow_down') {
2
rhythm = 'soft'
3
forbiddenMoves.push('premature_advice', 'pressure_to_disclose', 'intense_flirt')
4
questionLimit = Math.min(questionLimit, 1)
5
adviceLimit = Math.min(adviceLimit, 1)
6
sentenceBudget.max = Math.min(sentenceBudget.max, 3)
7
}

如果处于修复期,则优先修复关系体验:

index.ts
1
if (relationshipStage.pacing === 'repair_first') {
2
policy = 'relationship_repair'
3
rhythm = 'soft'
4
openingMove = 'apologize'
5
forbiddenMoves.push('intense_flirt', 'take_sides_aggressively', 'over_explain')
6
adviceLimit = 0
7
sentenceBudget.max = Math.min(sentenceBudget.max, 3)
8
}

注入最终 Prompt

关系阶段也会被注入最终系统提示词:

index.ts
01
function getRelationshipStageSystemInstruction(relationshipStage: ConversationRelationshipStage | null) {
02
if (!relationshipStage) {
03
return ''
04
}
05
06
return [
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}`,
15
relationshipStage.riskSignals.length > 0 ? `- 风险信号:${relationshipStage.riskSignals.join('、')}` : '',
16
`- 关系指导:${relationshipStage.relationshipGuidance}`,
17
'请把关系阶段作为隐性节奏控制:不要在回复中暴露阶段名称、分数或内部标签。',
18
].filter(Boolean).join('\n')
19
}

注意最后一句很重要:关系阶段是内部策略,不应该被用户直接看到。

写入消息 metadata

用户消息落库时,metadata 中会保存本轮完整理解结果:

index.ts
01
function toConversationAnalysisMetadata(params: {
02
safety: ConversationSafety
03
intent: ConversationIntent | null
04
emotion: ConversationEmotion | null
05
relationshipStage: ConversationRelationshipStage | null
06
route: EmotionRoute | null
07
replyPolicy: ReplyPolicy | null
08
}) {
09
return JSON.stringify({
10
analysisVersion: 'conversation-understanding-v2',
11
safety: params.safety,
12
intent: params.intent,
13
emotion: params.emotion,
14
relationshipStage: params.relationshipStage,
15
route: params.route,
16
replyPolicy: params.replyPolicy,
17
})
18
}

这样以后可以回看每一轮:

  • 当时处于什么关系阶段。
  • 为什么走了某条情绪路由。
  • 回复策略为什么限制亲密度、建议数量或追问数量。

首页 Inbox 展示

除了聊天链路,首页 Inbox 也做了轻量展示调整。

列表中原本所有已发布 Agent 都显示专属伴侣,现在会根据 agent_conversations.message_count 显示阶段:

index.ts
01
function getInboxRelationshipStage(messageCount: number) {
02
if (messageCount >= 80) {
03
return { relationship: '亲密连结' }
04
}
05
06
if (messageCount >= 36) {
07
return { relationship: '稳定信任' }
08
}
09
10
if (messageCount >= 16) {
11
return { relationship: '舒适陪伴' }
12
}
13
14
if (messageCount >= 6) {
15
return { relationship: '升温熟悉' }
16
}
17
18
return { relationship: '初识破冰' }
19
}

这里的展示逻辑是轻量版,只基于消息量。真正影响回复的关系阶段,仍然以后端聊天链路中的 LangGraph 判断为准。

为什么不做 D1 迁移

本次没有新增表,也没有新增字段。

原因是第一版关系阶段是每轮动态分析结果,它会写入消息 metadata_json 中,而不是作为单独业务状态保存。

这样可以避免过早把阶段固化到数据库里。关系阶段本来就会随着最近对话、情绪和边界变化而变化。如果未来要做长期稳定的关系成长系统,再考虑新增独立表,例如:

index.txt
1
agent_relationship_states

里面可以保存长期亲密度、阶段变更历史、阶段进入时间、阶段升级/降级原因等。

后续升级

第一版已经能影响回复,但还可以继续增强。

阶段历史

后续可以记录每次阶段变化,比如从初识到升温,从稳定信任降到修复期,或者从依赖观察恢复到舒适陪伴。

这可以用于分析 Agent 关系体验是否健康。

阶段可视化

前端可以在 Agent 详情页展示当前关系阶段、亲近度、信任等级和最近阶段变化。

但不建议把太多内部指标直接展示给普通用户,否则会让陪伴感变成游戏数值感。

主动消息

当关系进入稳定信任或亲密连结后,可以允许 Agent 在合适时机生成更自然的主动问候。

但这必须和安全边界、频率控制、用户设置一起做,不能变成打扰。

成长事件

可以设计一些非模板化的成长事件,例如:

  • 第一次记住用户的重要偏好。
  • 第一次完成一次情绪修复。
  • 第一次共同完成一个长期目标。

这些事件可以推动关系阶段变化,而不是只靠消息数量。

总结

关系阶段系统让 AI 电子伴侣从单轮回复工具更接近持续互动对象

它不会直接决定一句话怎么说,而是给后续情绪路由和回复策略提供关系节奏:什么时候要慢一点,什么时候可以自然靠近,什么时候要先修复,什么时候必须守住边界。

第一版使用 LangChain 结构化输出和 LangGraph 编排完成动态判断,并通过启发式兜底保证稳定性。它不新增 D1 迁移,而是把每轮阶段结果写入消息 metadata,先建立可追踪的关系理解闭环。