Skip to content

Agent 系统 — 情感计算引擎(基于 Google ADK)

定位

Shice AI 的核心是一个人际关系情感计算引擎:它对关系中各方的情绪状态、依恋风格、权力动态进行建模,并基于这些模型给出可执行的策略和话术。

Agent 系统使用 Google Agent Development Kit (ADK) for Go 构建,深度集成 ADK 的 Runner、Session、Memory 三大核心能力,通过自定义 model.LLM 适配器对接 OpenAI 兼容 API。

与其它文档的分工module_design_notes.md 的「核心数据流」概括 Handler → System;本文聚焦 internal/agent 包内的路由、Runner、Pipeline、Prompt 与异步情绪提取。SSE 事件形状与 profileProposalsse.mdhandler/chat.go


文件结构

backend/internal/agent/
  ├── types.go                  # StreamChunk, AgentContext, PersonData, Intent …
  ├── adk_model.go              # OpenAIModel / MockModel / ContextAwareModel — model.LLM
  ├── llm.go                    # Mock 固定回复等
  ├── prompts.go                # productPersona、各 Agent system prompt、chatRouterSystemPrompt
  ├── agents.go                 # llmagent.New 四子 Agent + newOrchestratorAgent + runDirectAgent
  ├── orchestrator.go         # detectIntent()关键词表(独立工具/兜底,非 Chat 主路由)
  ├── chat_pipeline.go          # decideChatRoute、两固定 Pipeline、Router 的 Langfuse step
  ├── system.go                 # System门面:Chat / Analyze / Advise / Script;Runner + Memory
  ├── emotion.go                # EmotionExtractor → emotion_snapshots
  ├── trace_llm.go              # 子调用步骤收集、Langfuse 分组上报
  ├── langfuse_context.go       # trace id / steps 在 context 中的传递
  ├── profile_update_tool.go    # Orchestrator 可挂载的档案更新工具(ADK tool)
  ├── profile_extract.go        # 对话后画像/档案提案抽取(与 Chat Handler 门控配合)
  ├── profile_*.go              # 提案注册、用户意图 LLM、观测记录等

backend/internal/observability/
  └── langfuse.go               # CompletionRecorder

Chat 端到端工作链路(概念)

HTTP POST /api/v1/chat/stream(Gin ChatHandler)
  ├── 额度(quota)与上下文加载(人物、历史、session summary、情绪快照、self profile …)
  ├── 组装 *agent.AgentContext(locale、max_tokens、Model、History …)
  └── agent.System.Chat(ctx, userText, agentCtx)

        ├──① 【Router LLM】decideChatRoute — 单次非流式调用,输出 JSON {"mode":"..."}
        │       • runner        │       • pipe_analyze_strategist
        │       • pipe_strategist_writer
        │     (规则见 prompts.go → chatRouterSystemPrompt;解析失败则视为 runner)

        ├──②a mode =固定 Pipeline(不经过 ADK Runner)
        │       • pipe_analyze_strategist:Analyzer →(pipelinePreamble)→ Strategist,两段均 runDirectAgent 流式
        │       • pipe_strategist_writer:Strategist →(writerPipelinePreamble)→ Writer

        └──②b mode = runner
              └── adkRunner.Run(…, StreamingModeSSE)
                    └── Orchestrator(+ 可选 profile 工具)→ transfer_to_agent → 单个子 Agent 流式输出

Strategy 专用接口/strategy/analyze|advise|script不走路由器、不走 RunnerSystem.Analyze / Advise / Script 直接 runDirectAgent 到对应 Agent。


架构(System 内部)

System (门面)
  ├── ChatRouter(chat_pipeline.go)
  │     └── decideChatRoute → runner | pipe_analyze_strategist | pipe_strategist_writer
  ├── ContextAwareModel(按请求/Agent 切换底层模型)
  ├── ADK Runner(仅 runner 模式)
  │     ├── runner.Run(…, StreamingModeSSE)
  │     ├── OrchestratorAgent(SubAgents + Tools,InstructionProvider)
  │     │     └── transfer_to_agent → profile / analyzer / strategist / writer
  │     └── 四子 Agent:InstructionProvider 动态拼 prompt(注入 AgentContext)
  ├── 固定 Pipeline(两模式)
  │     └── 两段 runDirectAgent,中间用 PipelinePreamble 注入下游 prompt
  ├── Direct Agent(Strategy API)
  │     └── Analyze / Advise / Script → runDirectAgent
  ├── ADK Session + Memory(InMemory)
  ├── Session Summary(滚动摘要 → chat_sessions.summary,由 Handler/服务层触发,见业务代码)
  └── EmotionExtractor(异步,非 Mock 时)
        → emotion_snapshots,下轮注入 Emotional trajectory

ADK 集成方式

model.LLM 适配器

项目实现了 ADK 的 model.LLM 接口来对接 OpenAI 兼容 API:

go
// ADK 要求的接口
type LLM interface {
    Name() string
    GenerateContent(ctx context.Context, req *LLMRequest, stream bool) iter.Seq2[*LLMResponse, error]
}
实现说明
OpenAIModel通过 net/http 调用 OpenAI Chat Completions API,支持流式 SSE
MockModel无需 API Key 的本地 Mock,返回固定中文回复

OpenAIModel.WithModel(name) 可创建使用不同模型的副本,用于用户指定模型切换。

Agent 定义

每个子 Agent 使用 ADK 的 llmagent.New() 定义:

go
agent, _ := llmagent.New(llmagent.Config{
    Name:        "analyzer",
    Description: "Behavioral decoding: read subtext, model emotions, analyze power dynamics",
    Model:       NewContextAwareModel(model),
    InstructionProvider: func(ctx adkagent.ReadonlyContext) (string, error) {
        agentCtx, _ := ctx.Value(agentContextKey).(*AgentContext)
        return analyzerSystemPrompt(agentCtx), nil
    },
})

每个 Agent 使用 InstructionProvider(而非静态 Instruction)动态生成 system prompt,从 Go context.Value 读取 AgentContext(人物、语言、自我画像等)。模型包装为 ContextAwareModel 以支持前端按请求切换模型。

Strategy 端点(Analyze/Advise/Script)仍通过 runDirectAgent() 直接调用 model.LLM.GenerateContent(),绕过 Runner。

ADK Runner — Chat 的 runner 模式

当且仅当 Router 返回 mode: "runner" 时,System.Chat 调用 adkRunner.Run()

  1. Orchestrator 接收用户消息(可调用挂载的 profile 更新类 tool,再 transfer_to_agent
  2. 通过 ADK transfer_to_agent 将执行交给单个子 Agent
  3. 子 Agent 流式输出经 StreamChunk 传给 Gin SSE

Runner 配置(system.go):

  • AppName: "shice-ai"
  • Agent: orchestratorAgent(四 SubAgents + 可选 Tools)
  • SessionService / MemoryServiceInMemoryService()
  • AutoCreateSession: true
  • RunConfig.StreamingMode: StreamingModeSSE

ADK Session

使用 ADK 的 session.InMemoryService() 管理 Agent 层面的会话状态。每次对话的 user/assistant 消息通过 AppendEvent() 追加到 ADK Session,为 Memory 检索提供数据源。

ADK Memory(长期记忆)

使用 ADK 的 memory.InMemoryService() 实现跨会话的长期记忆:

用户发消息 → SearchMemory(query=用户消息) → 检索相关历史记忆

                                         注入 prompt "Long-term memory"

AI 回复完成 → AddToMemory(session) → 当前对话存入记忆库
  • 存储:每轮对话结束后,user/assistant 消息被追加到 ADK Session,然后整个 Session 通过 AddSessionToMemory() 添加到记忆服务
  • 检索:新对话时,用用户消息作为 query 调用 SearchMemory(),返回的相关记忆注入到 prompt 的 "Long-term memory" 部分
  • 范围:按 userID 隔离,跨所有会话和人物

注意:当前使用 InMemory 实现,服务重启后记忆清空。生产环境可替换为持久化实现(如基于 Qdrant 向量数据库)。


会话摘要(Session Summary)

每轮对话完成后异步生成/更新:

  1. sessionSummaryPrompt + 已有摘要 + 最新消息调用 LLM
  2. 生成 2-5 句话的滚动摘要,覆盖:讨论对象、核心问题、关键建议、行动项、情绪走向
  3. 存入 chat_sessions.summary 字段

跨会话上下文注入优先级

1. Session Summary(优先)→ LLM 生成的 2-5 句摘要
2. Raw Excerpts(兜底)  → 无摘要时截取前 4 条消息 × 120 字
3. Emotional Trajectory  → 最近 5 条情绪快照
4. Long-term Memory      → ADK Memory 关键词检索的相关记忆

Chat Router LLM(decideChatRoute)

每一次 System.Chat 开头,先用 独立 system promptchatRouterSystemPrompt)做一次非流式小调用,模型须只输出 JSON:{"mode":"runner"|"pipe_analyze_strategist"|"pipe_strategist_writer"}

语义规则(摘录,完整见 prompts.go):

mode含义
runner默认;交给 Orchestrator + transfer_to_agent一个子 Agent
pipe_analyze_strategist用户同一条消息里既要深度解读又要可执行方案
pipe_strategist_writer用户同一条消息里既要策略又要可直接粘贴的话术

若用户明确要更新/保存档案或画像(应用内结构化数据),Router 强制 runner,以便 Orchestrator 走工具链路,不走固定 Pipeline。

解析失败或异常时回退为 runner。Langfuse 中将该步记为 router(见 chat_pipeline.go)。


Orchestrator(Runner 模式内部)

runner 模式下,由 OrchestratorInstructionProviderorchestratorRoutingPrompt)+ ADK 的 transfer_to_agent 完成第二轮路由:

  1. 描述四个子 Agent 的分工
  2. LLM 选择 transfer_to_agent 目标:profile / analyzer / strategist / writer
  3. 选中 Agent 流式回复用户

orchestrator.go 中的 **detectIntent(关键词表)** 不驱动上述 Chat 主路径,保留作辅助/分析用途。


Pipeline 串联(已实现)

当 Router 选择固定 Pipeline 时,不调用 adkRunner.Run,而是顺序执行两段 runDirectAgent(两段均向同一 StreamChunk channel 写流式片段):

Pipeline第一段第二段衔接
pipe_analyze_strategistAnalyzerAgentStrategistAgent第一段全文写入 agentCtx.PipelinePreamblepipelinePreamble
pipe_strategist_writerStrategistAgentWriterAgent第一段写入 writerPipelinePreamble

与「用户分两轮提问」无关;一轮用户消息内即可得到两段合成体验。

未来若要用 ADK sequentialagent 或把 Agent 封装为 tool 在 Orchestrator 内多步调用,可作为实现替换,产品语义上与当前固定 Pipeline 等价。


Prompt 设计

productPersona(共享基座)

  • 情感计算引擎定位:对方情绪建模 + 依恋风格推断 + 预测反应
  • 情绪感知规则:高焦虑/愤怒时先 1-2 句接住情绪,再分析
  • 信息密度自适应:输入一句话 → 回 3-8 句;长输入 → 完整分析
  • 伦理约束:不鼓励操控/欺骗

各 Agent 独立框架

Agent分析维度
ProfileAgent性格与沟通 / 依恋与冲突风格 / 需求价值 / 关系动态(阶段 + 权力 + 信任)/ 互动指南
AnalyzerAgent表面 vs 真实意图 / 情绪建模(含依恋行为)/ 权力动态 / 潜台词 / 文化语境 / 推荐回应
StrategistAgent局势 / 目标 / 分阶段计划(含 IF/THEN 条件分支)/ 风险地图 / 成功信号
WriterAgent语言风格匹配(用户 SelfProfile 或推断)/ 3 变体 / 发送建议

动态注入

  • buildPersonContext — 关联人物档案(姓名/关系/标签/状态/备注)
  • buildSelfProfileSection — 用户自我画像
  • followUpInstruction — 追问时切换简洁格式
  • replyLanguageInstruction — 回复语言(zh / en)
  • outputBudgetInstructionmax_tokens 预算
  • pipelinePreamble — Pipeline 上游分析结果
  • RecentInteractions — 近期互动摘要 / Session Summary / Long-term Memory
  • EmotionSnapshot — 情绪轨迹

情绪追踪 (EmotionExtractor)

每次对话后异步运行(仅非 MockModel):

  1. emotionExtractPrompt + 用户消息 + 助手回复通过 model.LLM.GenerateContent() 发一次轻量调用
  2. 提取 JSON:{ user_mood, other_mood, trend, tags }
  3. 存入 emotion_snapshots 表,关联 session + person
  4. 下次对话加载最近 5 条快照,注入 prompt 的 RecentInteractions

配置示例

yaml
ai:
  default_provider: "openai"
  default_model: "gpt-4o-mini"
  api_key: "sk-..."
  agents:
    orchestrator: { provider: "openai", model: "gpt-4o-mini" }
    profile:      { provider: "openai", model: "gpt-4o-mini" }
    analyzer:     { provider: "openai", model: "gpt-4o-mini" }
    strategist:   { provider: "openai", model: "gpt-4o-mini" }
    writer:       { provider: "openai", model: "gpt-4o-mini" }

每个 Agent 可独立配置 provider / model / base_url / api_key,支持混合部署(如 Orchestrator 用轻量模型,Strategist 用重量级模型)。

Langfuse 观测见 internal/observability/langfuse.go;由 cmd/server 构造 CompletionRecorder 注入 NewSystem