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

概述

AI 电子伴侣聊天中的意图识别:用 LangChain 和 LangGraph 落地

在 AI 电子伴侣产品里,用户发来的一句话往往不只是问问题。同样一句我今天好累,可能是在求安慰,也可能只是想继续聊下去;可能是在表达现实生活里的压力,也可能希望 Agent 主动关心一下,甚至希望系统记住自己最近的状态。

如果聊天系统直接把用户输入丢给大模型,模型通常也能回复,但回复策略会摇摆不定。有时像客服,有时像心理咨询,有时又急着给一大段建议。对 AI 电子伴侣来说,我们可以在真正回复之前先做一层意图识别,把用户当前更需要什么分析出来,再把这个结果作为隐藏策略注入最终回复。

这篇文章我们就把这套已经落地的实现梳理一下:在 API 子站聊天接口中,用 LangChain 结构化输出 做意图分类,用 LangGraph 编排意图判断流程,把结果写入聊天消息 metadata_json,最后再注入到 Agent 回复 prompt 中。

目标

这次不是要重做一个复杂的智能体系统,而是在现有聊天链路里补一层轻量、稳定的对话理解。请求进来以后,危险内容先由安全边界拦住;安全通过的消息再进入 LangGraph 意图识别流程;模型通过 LangChain withStructuredOutput 输出稳定 JSON;识别结果既会写入用户消息 metadata_json,方便后续分析和调试,也会进入最终聊天 prompt,让 Agent 的回应更贴近陪伴场景。

整体链路可以这样看:

code.ts
01
flowchart TD
02
A["用户发送消息"] --> B["加载 Agent 信息、历史消息、长期记忆"]
03
B --> C["安全边界判断"]
04
C --> D{"是否需要拒绝或危机回复"}
05
D -- "是" --> E["返回边界回复并落库"]
06
D -- "否" --> F["LangGraph 意图判断"]
07
F --> G["写入用户消息 metadata"]
08
G --> H["组装最终聊天 prompt"]
09
H --> I["调用 LLM 流式回复"]
10
I --> J["保存 assistant 消息"]
11
J --> K["后续记忆抽取"]

先过安全边界

安全边界和意图判断解决的是两类问题。

安全边界先判断的是:这句话能不能继续聊,应该怎么限制。例如自伤危机、违法请求、隐私侵犯、操控关系等场景,系统必须先保护用户和他人。

意图判断再分析的是:这句话希望 Agent 怎样回应。例如用户是想要安慰、建议、暧昧互动、角色扮演,还是想调整 Agent 的说话方式。

所以落地顺序是:

  1. 先做安全边界判断。
  2. 如果安全边界要求 refusecrisis_support,直接返回边界回复。
  3. 如果可以继续聊天,再进入意图判断。

这样处理可以避免一个真实问题:当用户输入本身高风险时,系统还在认真分析用户是不是想要情绪陪伴,从而冲淡安全策略。

依赖接入

API 子站新增了 LangGraph 依赖:

index.json
1
{
2
"dependencies": {
3
"@langchain/core": "^1.1.48",
4
"@langchain/langgraph": "^1.4.1",
5
"@langchain/openai": "^1.4.7"
6
}
7
}

这三个包的分工很清楚。@langchain/core 提供 prompt、runnable 等基础能力;@langchain/openai 负责兼容 OpenAI 协议和 Responses 协议的聊天模型调用;@langchain/langgraph 则负责把意图判断拆成更容易维护的图节点。

本次没有新增 D1 迁移,因为之前聊天消息表已经有 metadata_json 字段,可以直接保存分析结果。

意图分类设计

AI 电子伴侣不是客服系统,因此意图分类不能停留在问答、投诉、咨询这种传统分类上。我们定义了一组更贴近陪伴、交友、恋爱互动的意图:

index.ts
01
const CompanionIntentPrimarySchema = z.enum([
02
'casual_chat',
03
'emotional_support',
04
'relationship_advice',
05
'romantic_flirt',
06
'companionship_presence',
07
'roleplay',
08
'life_sharing',
09
'memory_update',
10
'preference_setting',
11
'agent_feedback',
12
'conversation_repair',
13
'date_or_activity_planning',
14
'creative_request',
15
'meta_question',
16
'unclear',
17
])

为了方便后面使用,我们可以先按用途理解这些意图:

  • 陪伴类:casual_chatcompanionship_presencelife_sharing
  • 情绪类:emotional_supportconversation_repair
  • 关系类:relationship_adviceromantic_flirt
  • 玩法类:roleplaycreative_request
  • 配置类:memory_updatepreference_settingagent_feedback
  • 兜底类:meta_questionunclear

这里要特别小心:不要把所有带情绪的输入都分类成建议。很多时候用户并不想听解决方案,只是想被接住。

结构化输出 Schema

意图判断不能只返回一个字符串,否则后面很难稳定使用。最终使用 Zod 定义完整结构:

index.ts
01
const ConversationIntentSchema = z.object({
02
primary: CompanionIntentPrimarySchema,
03
secondary: z.array(CompanionIntentPrimarySchema).max(3),
04
confidence: z.number().min(0).max(1),
05
userNeed: z.enum([
06
'be_heard',
07
'be_comforted',
08
'get_advice',
09
'get_reply_draft',
10
'play_along',
11
'feel_connected',
12
'set_boundary',
13
'update_memory',
14
'adjust_agent',
15
'unknown',
16
]),
17
requestedAgentAction: z.enum([
18
'answer_directly',
19
'comfort_first',
20
'ask_follow_up',
21
'draft_message',
22
'analyze_situation',
23
'roleplay_response',
24
'remember_fact',
25
'adjust_style',
26
'repair_misunderstanding',
27
'continue_topic',
28
]),
29
relationshipSignal: z.enum([
30
'neutral',
31
'warming_up',
32
'seeking_closeness',
33
'testing_boundary',
34
'feeling_hurt',
35
'pulling_away',
36
'dependency_risk',
37
'conflict',
38
]),
39
replyExpectation: z.object({
40
depth: z.enum(['short', 'medium', 'deep']),
41
warmth: z.enum(['low', 'medium', 'high']),
42
directness: z.enum(['gentle', 'balanced', 'direct']),
43
shouldAskQuestion: z.boolean(),
44
}),
45
shouldClarify: z.boolean(),
46
clarifyingQuestion: z.string().trim().max(200).nullable(),
47
promptGuidance: z.string().trim().max(600),
48
})

这个结构里不能只看 primaryuserNeed 记录用户真正想被如何对待,requestedAgentAction 记录 Agent 应该采取什么回复动作,relationshipSignal 描述当前关系氛围是升温、疏远、冲突,还是依赖风险。replyExpectation 用来控制回复长度、温度和直接程度,promptGuidance 则给最终回复模型一段简短策略。

所以意图判断不只是打标签,它要直接服务后面的回复生成。

Prompt 设计

意图判断的 prompt 会先把职责讲清楚:你不是来聊天的,而是来分析用户意图的。

核心 prompt 如下:

index.ts
01
const conversationIntentPrompt = ChatPromptTemplate.fromMessages([
02
[
03
'system',
04
[
05
'你是 AI 电子伴侣聊天产品的意图识别器。',
06
'你的任务不是回复用户,而是判断用户在当前亲密陪伴/交友聊天场景中的真实沟通意图。',
07
'必须结合最近对话、长期记忆、Agent 人设边界和安全边界结果来判断。',
08
'优先区分:普通闲聊、情绪陪伴、恋爱暧昧、角色扮演、生活分享、关系建议、记忆更新、偏好设置、对 Agent 的反馈、误会修复。',
09
'不要把所有问题都归为关系建议;用户只是想被陪伴、被听见或维持互动时,要识别为陪伴类意图。',
10
'当用户表达模糊但情绪明确时,先判断情绪和期待,再决定是否需要追问。',
11
'输出必须是可被 LangChain 结构化解析的 JSON 对象。',
12
].join('\n'),
13
],
14
[
15
'human',
16
[
17
'Agent 名称:{agentName}',
18
'',
19
'Agent 自定义边界规则:',
20
'{agentGuardrails}',
21
'',
22
'安全边界判断:',
23
'{safety}',
24
'',
25
'长期记忆:',
26
'{activeMemories}',
27
'',
28
'最近对话:',
29
'{recentMessages}',
30
'',
31
'本轮用户输入:',
32
'{userText}',
33
].join('\n'),
34
],
35
])

这里输入的不只有本轮用户文本,还包括 Agent 名称、Agent 自定义边界规则、当前安全边界结果、当前用户和 Agent 的长期记忆,以及最近聊天消息。上下文越完整,意图判断越不容易只看字面意思。

为什么要把安全边界结果也传进去?因为有些意图和风险是交织在一起的。例如用户明显出现强情绪依赖时,意图可能仍然是寻求陪伴,但最终回复不能强化依赖,需要更克制。

LangChain 结构化输出

意图判断调用模型时,没有让模型自由输出文本,而是使用 withStructuredOutput

index.ts
01
async function invokeConversationIntentAnalysis(params: {
02
method: LangChainStructuredOutputMethod
03
providerConfig: ChatProviderConfig
04
agentName: string
05
agentGuardrails: string | null
06
safety: ConversationSafety
07
activeMemories: StoredAgentMemory[]
08
recentMessages: Array<{ role: 'user' | 'assistant'; content: string }>
09
userText: string
10
signal?: AbortSignal
11
}) {
12
const model = buildLangChainChatModel(params.providerConfig)
13
const structuredModel = model.withStructuredOutput(ConversationIntentSchema, {
14
name: 'conversation_intent_analysis',
15
method: params.method,
16
})
17
const chain = conversationIntentPrompt.pipe(structuredModel)
18
19
const result = await chain.invoke({
20
agentName: params.agentName || '未命名 Agent',
21
agentGuardrails: params.agentGuardrails || '暂无',
22
safety: formatSafetyForPrompt(params.safety),
23
activeMemories: formatExistingMemories(params.activeMemories),
24
recentMessages: formatRecentMessages(params.recentMessages),
25
userText: params.userText,
26
}, params.signal ? { signal: params.signal } : undefined)
27
28
return normalizeConversationIntent(ConversationIntentSchema.parse(result), params.safety)
29
}

这里我们可以多看两个细节。

第一,withStructuredOutput 仍然可能因为不同模型、不同中转协议支持度不同而失败,所以外层做了多方法重试。

index.ts
1
function getStructuredOutputMethods(providerConfig: ChatProviderConfig) {
2
return providerConfig.wireApi === 'responses'
3
? ['jsonSchema', 'functionCalling', 'jsonMode'] as const
4
: ['functionCalling', 'jsonSchema', 'jsonMode'] as const
5
}

Responses 协议优先尝试 jsonSchema,Chat Completions 协议优先尝试 functionCalling。这样对不同三方中转 API 更友好。

第二,结构化输出之后仍然要再走一层 normalizeConversationIntent。不要完全相信模型输出,尤其是涉及置信度、追问和依赖风险时,代码层要继续兜底。

LangGraph 编排

这次 LangGraph 先没有做复杂多分支,而是从一个轻量图开始:

index.ts
01
const ConversationIntentState = 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
userText: Annotation<string>(),
09
normalizedInput: Annotation<string>(),
10
intent: Annotation<ConversationIntent | null>(),
11
signal: Annotation<AbortSignal | undefined>(),
12
})

图里目前只有两个节点。

第一个节点负责归一化用户输入:

index.ts
1
function normalizeIntentInputNode(state: typeof ConversationIntentState.State) {
2
return {
3
normalizedInput: normalizeStoredMessage(state.userText),
4
}
5
}

第二个节点负责调用 LangChain 意图识别:

index.ts
01
async function classifyIntentNode(state: typeof ConversationIntentState.State) {
02
const userText = state.normalizedInput || normalizeStoredMessage(state.userText)
03
04
if (!userText) {
05
return {
06
intent: normalizeConversationIntent({
07
...fallbackIntent,
08
primary: 'casual_chat',
09
confidence: 0.7,
10
userNeed: 'feel_connected',
11
requestedAgentAction: 'continue_topic',
12
shouldClarify: false,
13
clarifyingQuestion: null,
14
promptGuidance: '用户没有提供明确新内容时,轻柔延续当前话题,不要制造压力。',
15
}, state.safety),
16
}
17
}
18
19
return {
20
intent: await classifyConversationIntentWithLangChain({
21
providerConfig: state.providerConfig,
22
agentName: state.agentName,
23
agentGuardrails: state.agentGuardrails,
24
safety: state.safety,
25
activeMemories: state.activeMemories,
26
recentMessages: state.recentMessages,
27
userText,
28
signal: state.signal,
29
}),
30
}
31
}

最后把图编译出来:

index.ts
1
const conversationIntentGraph = new StateGraph(ConversationIntentState)
2
.addNode('normalizeInput', normalizeIntentInputNode)
3
.addNode('classifyIntent', classifyIntentNode)
4
.addEdge(START, 'normalizeInput')
5
.addEdge('normalizeInput', 'classifyIntent')
6
.addEdge('classifyIntent', END)
7
.compile()

这里可能会有一个疑问:只有两个节点,为什么还要用 LangGraph?

原因是这个位置后面很容易继续长大。比如情绪识别、回复策略路由、是否需要主动写入长期记忆、是否需要调用工具、是否进入角色扮演模式、是否触发关系阶段变化,这些判断都可能陆续接进来。

如果现在只是写一个普通函数,后面很容易堆成一大段难维护的条件判断。我们先用 LangGraph 把状态和节点边界定义好,后续扩展时就可以自然增加节点,而不用重写聊天链路。

失败兜底策略

意图判断不能影响主聊天可用性。也就是说,哪怕意图分析失败,聊天也应该继续。

所以实现里定义了 fallbackIntent

index.ts
01
const fallbackIntent: ConversationIntent = {
02
primary: 'unclear',
03
secondary: [],
04
confidence: 0.3,
05
userNeed: 'unknown',
06
requestedAgentAction: 'ask_follow_up',
07
relationshipSignal: 'neutral',
08
replyExpectation: {
09
depth: 'medium',
10
warmth: 'medium',
11
directness: 'gentle',
12
shouldAskQuestion: true,
13
},
14
shouldClarify: true,
15
clarifyingQuestion: '你是更想让我先听你说说,还是想让我帮你一起想办法?',
16
promptGuidance: '先简短承接用户,不要擅自下结论;用一个自然的问题澄清用户真正需要。',
17
}

当 LangChain 结构化输出失败,或者 LangGraph 执行异常时,就回到这个保守策略。

index.ts
01
async function analyzeConversationIntent(params: {
02
providerConfig: ChatProviderConfig
03
agentName: string
04
agentGuardrails: string | null
05
safety: ConversationSafety
06
activeMemories: StoredAgentMemory[]
07
recentMessages: Array<{ role: 'user' | 'assistant'; content: string }>
08
userText: string
09
signal: AbortSignal
10
}): Promise<ConversationIntent> {
11
try {
12
const result = await conversationIntentGraph.invoke({
13
providerConfig: params.providerConfig,
14
agentName: params.agentName,
15
agentGuardrails: params.agentGuardrails,
16
safety: params.safety,
17
activeMemories: params.activeMemories,
18
recentMessages: params.recentMessages,
19
userText: params.userText,
20
normalizedInput: '',
21
intent: null,
22
signal: params.signal,
23
}, { signal: params.signal })
24
25
return result.intent ?? normalizeConversationIntent(fallbackIntent, params.safety)
26
} catch (error) {
27
console.warn('LangGraph conversation intent analysis failed', error)
28
return normalizeConversationIntent(fallbackIntent, params.safety)
29
}
30
}

这个兜底策略比较克制:不瞎猜,先承接,再追问。

对于 AI 电子伴侣来说,这比错误地给建议更安全,也更符合陪伴体验。

归一化规则

模型输出之后,代码会做一次归一化:

index.ts
01
function normalizeConversationIntent(intent: ConversationIntent, safety: ConversationSafety): ConversationIntent {
02
const next: ConversationIntent = {
03
...intent,
04
secondary: Array.from(new Set(intent.secondary.filter((item) => item !== intent.primary))).slice(0, 3),
05
replyExpectation: { ...intent.replyExpectation },
06
clarifyingQuestion: intent.clarifyingQuestion?.trim() || null,
07
promptGuidance: intent.promptGuidance.trim(),
08
}
09
10
if (next.confidence < 0.45) {
11
next.primary = 'unclear'
12
next.secondary = []
13
next.userNeed = 'unknown'
14
next.requestedAgentAction = 'ask_follow_up'
15
next.shouldClarify = true
16
next.replyExpectation.shouldAskQuestion = true
17
}
18
19
if (next.primary === 'memory_update') {
20
next.userNeed = 'update_memory'
21
next.requestedAgentAction = 'remember_fact'
22
next.replyExpectation.depth = 'short'
23
next.replyExpectation.shouldAskQuestion = false
24
next.shouldClarify = false
25
}
26
27
if (next.primary === 'preference_setting' || next.primary === 'agent_feedback') {
28
next.userNeed = 'adjust_agent'
29
next.requestedAgentAction = next.primary === 'agent_feedback' ? 'repair_misunderstanding' : 'adjust_style'
30
}
31
32
if (safety.category === 'emotional_dependency' || safety.boundaryAction === 'soft_boundary') {
33
next.relationshipSignal = next.relationshipSignal === 'neutral' ? 'dependency_risk' : next.relationshipSignal
34
next.promptGuidance = [
35
next.promptGuidance,
36
'注意不要强化过度依赖,回复要温和陪伴,同时鼓励用户保留现实支持和自主判断。',
37
].filter(Boolean).join(' ')
38
}
39
40
if (next.shouldClarify && !next.clarifyingQuestion) {
41
next.clarifyingQuestion = fallbackIntent.clarifyingQuestion
42
}
43
44
if (!next.promptGuidance) {
45
next.promptGuidance = fallbackIntent.promptGuidance
46
}
47
48
return next
49
}

这层规则是在帮模型输出继续收口,主要处理几类实际问题:

  • 次要意图不能和主要意图重复。
  • 低置信度结果统一降级为 unclear
  • 记忆更新类意图不需要继续追问。
  • Agent 反馈类意图要转换成调整风格或修复误会。
  • 如果安全边界提示存在情绪依赖风险,回复策略要自动变克制。

这就是 LLM 判断 + 代码治理 的组合。LLM 负责理解语义,代码负责守住产品策略。

接入聊天主流程

在聊天接口 POST /rpc/chat/inbox 中,核心接入顺序如下:

index.ts
01
const safety = await analyzeConversationSafety({
02
providerConfig,
03
agentName: payload.conversation.name,
04
agentGuardrails: agentPrompt?.guardrailsPrompt ?? null,
05
activeMemories,
06
recentMessages: storedRecentMessages,
07
userText: latestUserText,
08
signal: c.req.raw.signal,
09
})
10
11
const boundaryResponse = buildBoundaryResponse(safety)
12
13
const intent = boundaryResponse
14
? null
15
: await analyzeConversationIntent({
16
providerConfig,
17
agentName: payload.conversation.name,
18
agentGuardrails: agentPrompt?.guardrailsPrompt ?? null,
19
safety,
20
activeMemories,
21
recentMessages: storedRecentMessages,
22
userText: latestUserText,
23
signal: c.req.raw.signal,
24
})

这段接入逻辑需要注意三个顺序。安全判断永远先于意图判断;如果已经要返回边界回复,意图判断就没有意义;意图判断使用和最终聊天同一个用户配置的 LLM,这样模型行为和用户配置保持一致。

保存 metadata

用户消息落库时,把安全边界和意图判断一起保存:

index.ts
01
function toConversationAnalysisMetadata(params: {
02
safety: ConversationSafety
03
intent: ConversationIntent | null
04
}) {
05
return JSON.stringify({
06
analysisVersion: 'conversation-analysis-v1',
07
safety: params.safety,
08
intent: params.intent,
09
})
10
}

保存时使用:

index.ts
01
await insertAgentConversationMessage({
02
db,
03
id: sourceUserMessageId,
04
conversationId,
05
userId: claims.sub,
06
agentId,
07
role: 'user',
08
content: latestUserText,
09
status: 'completed',
10
metadataJson: toConversationAnalysisMetadata({ safety, intent }),
11
nowMs: userMessageNowMs,
12
})

把这些分析结果留下来,后面会省很多排查成本。我们可以在后台分析用户主要意图分布,也可以回看为什么某一轮回复突然变短或者变温柔。后续要做情绪路由、关系阶段变化、记忆抽取时,这些 metadata 也能作为参考依据。分类体系需要迭代时,也不用只靠感觉改 prompt。

注入最终回复 Prompt

意图判断不是直接展示给用户的,而是变成系统 prompt 的一部分:

index.ts
01
function getIntentSystemInstruction(intent: ConversationIntent | null) {
02
if (!intent) {
03
return ''
04
}
05
06
return [
07
'本轮用户意图判断:',
08
`- 主要意图:${intent.primary}(置信度 ${intent.confidence.toFixed(2)})`,
09
intent.secondary.length > 0 ? `- 次要意图:${intent.secondary.join('、')}` : '',
10
`- 用户需要:${intent.userNeed}`,
11
`- 建议动作:${intent.requestedAgentAction}`,
12
`- 关系信号:${intent.relationshipSignal}`,
13
`- 回复期待:深度 ${intent.replyExpectation.depth},温度 ${intent.replyExpectation.warmth},直接程度 ${intent.replyExpectation.directness}`,
14
`- 是否追问:${intent.shouldClarify ? '是' : '否'}`,
15
intent.shouldClarify && intent.clarifyingQuestion ? `- 可用追问:${intent.clarifyingQuestion}` : '',
16
`- 回复指导:${intent.promptGuidance}`,
17
'请把以上意图判断作为隐性策略,不要在回复中暴露分类标签。',
18
].filter(Boolean).join('\n')
19
}

最终组装系统 prompt 时,把它放在安全边界之后、长期记忆之前:

index.ts
01
const messages: ChatCompletionMessage[] = [
02
{
03
role: 'system',
04
content: [
05
agentPrompt?.defaultPrompt || '你是 AI Agent Web 控制台里的聊天陪伴助手。',
06
'请基于当前聊天对象、关系氛围和用户意图,用简洁、自然的中文回答用户。',
07
'如果用户要求起草回复,请直接给出可发送的聊天内容,避免正式公文格式和职场汇报语气。',
08
'你的建议应尊重双方边界,避免操控式话术、制造焦虑或诱导过度解读。',
09
getSafetySystemInstruction(safety),
10
getIntentSystemInstruction(intent),
11
activeMemories.length > 0
12
? [
13
'以下是用户与该 Agent 的长期记忆,请优先尊重:',
14
...activeMemories.map((memory) => `- [${memory.type} / 重要度 ${memory.importance}] ${memory.content}`),
15
].join('\n')
16
: '',
17
ownedConversation?.conversation.summary
18
? `此前对话摘要:${ownedConversation.conversation.summary}`
19
: '',
20
].join('\n'),
21
},
22
]

这一步会明显影响最终回复。

例如用户说:

NOTE

今天有点烦,不想说话。

没有意图判断时,模型可能给一大段建议。
有意图判断后,它更可能识别为 emotional_supportcompanionship_presence,回复策略会变成短句、温柔、少追问。

和长期记忆的关系

意图判断和长期记忆不是同一件事。

长期记忆回答的是:

NOTE

这个用户有哪些稳定偏好、边界、关系设定和重要事实?

意图判断回答的是:

NOTE

用户这一轮到底想让 Agent 怎么回应?

两者一起使用,效果才稳定。

例如长期记忆里记录了用户不喜欢说教式建议,当前意图又判断为 emotional_support,那么最终回复就应该少讲道理,多共情。

反过来,如果当前意图是 memory_update,说明这轮消息本身可能值得进入记忆系统,后续可以把这个意图作为记忆抽取的参考信号。

为什么不用前端判断

意图判断没有放在前端,原因有三个。

第一,前端没有完整上下文。长期记忆、会话摘要、安全边界、Agent prompt 都在 API 侧更完整。

第二,前端判断不可信。用户可以绕过前端直接请求 API,核心策略应该放在服务端。

第三,后续要做数据分析。意图结果写入 D1 消息 metadata 后,后续可以在后台看分布、做调优、做路由。

所以前端只负责展示聊天体验,意图判断属于 API 的对话理解层。

当前版本的边界

这个版本只做意图判断,还没有做完整的情绪路由。

不过 schema 里已经预留了 relationshipSignalreplyExpectation

  • relationshipSignal 可以支持后续关系状态变化。
  • replyExpectation 可以支持后续回复风格路由。
  • requestedAgentAction 可以支持后续工具调用或 Agent 行为选择。

也就是说,这一版是在为后续的情绪路由行为路由打基础。

后续演进

下一步可以沿着 LangGraph 增加节点:

code.ts
1
flowchart TD
2
A["normalizeInput"] --> B["classifyIntent"]
3
B --> C["detectEmotion"]
4
C --> D{"routePolicy"}
5
D --> E["comfortReplyPolicy"]
6
D --> F["relationshipAdvicePolicy"]
7
D --> G["roleplayPolicy"]
8
D --> H["memoryUpdatePolicy"]

后面比较自然的演进方向有:

  1. 增加情绪识别节点,输出情绪类型、强度、是否需要低刺激回复。
  2. 增加回复策略路由节点,根据意图和情绪选择不同 prompt 模板。
  3. 增加记忆候选节点,判断当前消息是否应该进入长期记忆抽取。
  4. 增加关系状态节点,记录用户和 Agent 的关系阶段变化。
  5. 增加评估节点,对 assistant 回复做轻量自检。

这些都适合继续用 LangGraph 扩展,而不是把逻辑塞进一个越来越大的函数里。

总结

这次实现最重要的一点是:

NOTE

先理解用户想要什么,再让 Agent 回复。

回到实现上,我们可以把这一篇的内容再梳理一下:

  • 用安全边界判断守住底线。
  • 用 LangChain withStructuredOutput 输出稳定意图 JSON。
  • 用 LangGraph 把意图判断流程节点化。
  • 用代码归一化兜底模型不稳定输出。
  • safety + intent 写入消息 metadata。
  • 把意图作为隐藏策略注入最终聊天 prompt。

对于 AI 电子伴侣产品来说,意图判断不是锦上添花。它让 Agent 不只是能回答,而是逐渐变得懂得怎么回应