portrait_generation_graph.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. """用户画像生成/重画 LangGraph
  2. 输入(由 Java 传入):
  3. - user_id: 用户ID
  4. - member_id: 成员ID(可空)
  5. - previous_portrait: 旧画像文本(重画时传入,首次生成为 None)
  6. - profile_snapshot: 五维画像快照 dict(含 member/dimension_scores/body_metrics/mind_metrics/problem_domains)
  7. - behavior_summary: 行为摘要 dict
  8. 输出:
  9. - portrait_text: 生成的画像文本(中文,自然语言描述)
  10. """
  11. from typing import TypedDict
  12. from langgraph.graph import StateGraph, START, END
  13. from langchain_openai import ChatOpenAI
  14. from langchain_core.messages import SystemMessage, HumanMessage
  15. from app.config import settings
  16. import logging
  17. logger = logging.getLogger(__name__)
  18. class PortraitGenerationState(TypedDict):
  19. user_id: int
  20. member_id: int | None
  21. previous_portrait: str | None
  22. profile_snapshot: dict | None
  23. behavior_summary: dict | None
  24. portrait_text: str | None
  25. SYSTEM_PROMPT = """你是「浠艾福」家庭成长平台的 AI 画像分析师。
  26. 任务:根据用户的行为数据、五维能量指标、健康画像,生成一段自然语言的「用户画像」文本。
  27. 画像用途:作为后续所有 AI 对话(健康教练、管家、营养师、通用聊天)的 SystemMessage 注入,帮助 AI 更懂用户,给出更个性化的建议。
  28. 生成要求:
  29. 1. 语言自然、温暖、具体,像一位了解家庭的成长顾问写的观察笔记
  30. 2. 必须包含:核心特质、行为模式、关注点、潜在需求
  31. 3. 长度:200-500 字,不要过长
  32. 4. 不要列举原始数据(如"身 7.2 智 6.5"),要转化为洞察(如"身体维度相对突出,但智力维度有提升空间")
  33. 5. 如有 previous_portrait,重画时要体现"变化"与"延续":哪些特质稳定,哪些有新变化,原因可能是什么
  34. 输出格式:纯文本,无 JSON,无标记。"""
  35. def create_portrait_generation_graph():
  36. llm = ChatOpenAI(
  37. model=settings.llm_model,
  38. api_key=settings.llm_api_key,
  39. base_url=settings.llm_base_url,
  40. temperature=0.7,
  41. )
  42. builder = StateGraph(PortraitGenerationState)
  43. async def generate_portrait(state: PortraitGenerationState) -> dict:
  44. previous = state.get("previous_portrait")
  45. profile = state.get("profile_snapshot") or {}
  46. behavior = state.get("behavior_summary") or {}
  47. context_parts = []
  48. if profile:
  49. member = profile.get("member") or {}
  50. name = member.get("name", "用户")
  51. age = member.get("age")
  52. gender = member.get("gender")
  53. context_parts.append(f"用户:{name},{age}岁,{gender}")
  54. dims = profile.get("dimension_scores") or {}
  55. if dims:
  56. context_parts.append(
  57. f"五维评分:身 {dims.get('body','?')} 智 {dims.get('wisdom','?')} "
  58. f"心 {dims.get('mind','?')} 行 {dims.get('action','?')} 富 {dims.get('wealth','?')}"
  59. )
  60. body = profile.get("body_metrics") or {}
  61. if body.get("sleep_dur_avg"):
  62. context_parts.append(f"平均睡眠:{body['sleep_dur_avg']}h/天")
  63. if body.get("exercise_count_week"):
  64. context_parts.append(f"周运动:{body['exercise_count_week']}次")
  65. mind = profile.get("mind_metrics") or {}
  66. if mind.get("stress_avg"):
  67. context_parts.append(f"平均压力:{mind['stress_avg']}/10")
  68. problems = profile.get("problem_domains") or []
  69. if problems:
  70. context_parts.append(f"关注问题:{', '.join(problems[:5])}")
  71. if behavior:
  72. context_parts.append("行为摘要:" + str(behavior))
  73. context_text = "\n".join(context_parts) or "暂无额外数据"
  74. if previous:
  75. human_content = f"""【重画任务】请基于以下最新数据,重新生成用户画像。
  76. 【原画像(参考)】:
  77. {previous}
  78. 【最新数据】:
  79. {context_text}
  80. 要求:保留原画像中仍有效的核心特质,更新已变化的部分,体现"变化与延续"。"""
  81. else:
  82. human_content = f"""【生成任务】请基于以下数据,生成用户画像。
  83. 【数据】:
  84. {context_text}"""
  85. messages = [
  86. SystemMessage(content=SYSTEM_PROMPT),
  87. HumanMessage(content=human_content),
  88. ]
  89. response = await llm.ainvoke(messages)
  90. portrait_text = response.content.strip()
  91. logger.info("生成画像完成: user_id=%d, member_id=%s, len=%d",
  92. state["user_id"], state.get("member_id"), len(portrait_text))
  93. return {"portrait_text": portrait_text}
  94. builder.add_node("generate_portrait", generate_portrait)
  95. builder.add_edge(START, "generate_portrait")
  96. builder.add_edge("generate_portrait", END)
  97. return builder.compile()
  98. _portrait_graph = None
  99. def get_portrait_graph():
  100. global _portrait_graph
  101. if _portrait_graph is None:
  102. _portrait_graph = create_portrait_generation_graph()
  103. return _portrait_graph