health_butler_graph.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. """
  2. 健康管家 LangGraph - 专为儿童健康咨询设计的对话图
  3. Uses RAG retrieval from health knowledge base.
  4. """
  5. from typing import TypedDict
  6. from langgraph.graph import StateGraph, START, END
  7. from langgraph.checkpoint.memory import MemorySaver
  8. from langchain_openai import ChatOpenAI, OpenAIEmbeddings
  9. from langchain_core.messages import SystemMessage, HumanMessage
  10. from app.rag.retriever import RagRetriever
  11. from app.memory.store import MemoryManager
  12. from app.config import settings
  13. import logging
  14. logger = logging.getLogger(__name__)
  15. class HealthButlerState(TypedDict):
  16. query: str
  17. user_id: int
  18. child_id: int | None
  19. conversation_id: str | None
  20. context: dict | None
  21. answer: str | None
  22. sources: list[dict]
  23. memory_messages: list | None
  24. HEALTH_BUTLER_PROMPT = """你是一个专业、温暖的家庭健康顾问,专注于儿童健康成长。
  25. 你的职责:
  26. 1. 儿童体质调理咨询(如:挑食、瘦弱、易疲劳、反复生病)
  27. 2. 科学喂养和营养建议(辅食搭配、食欲不振、营养补充)
  28. 3. 日常保健知识(科学睡眠、运动建议、季节防护)
  29. 4. 体质辨识基础咨询(气虚/阳虚/阴虚/痰湿等体质特征解读)
  30. 5. 健康问题初步判断和就医建议
  31. 回答原则:
  32. 1. 用中文,语气温暖专业,像一个关心孩子的家庭医生
  33. 2. 涉及具体健康问题,先问清年龄、性别、症状持续时间等关键信息
  34. 3. 不确定时明确说"建议咨询专业医生",绝不臆断严重疾病
  35. 4. 回答结构:问题分析 → 建议方案 → 日常调养tip
  36. 5. 涉及儿童安全(性早熟、严重外伤、急性症状等)立刻建议就医
  37. 你可以通过检索健康知识库来获取依据。
  38. """
  39. def create_health_butler_graph():
  40. """创建健康管家 StateGraph"""
  41. # LLM
  42. llm = ChatOpenAI(
  43. model=settings.llm_model,
  44. api_key=settings.llm_api_key,
  45. base_url=settings.llm_base_url,
  46. temperature=settings.llm_temperature,
  47. )
  48. # 嵌入和检索
  49. embeddings = OpenAIEmbeddings(
  50. model=settings.embedding_model,
  51. api_key=settings.effective_embedding_api_key,
  52. base_url=settings.effective_embedding_base_url,
  53. )
  54. retriever = RagRetriever(collection_name="health_knowledge")
  55. memory_mgr = MemoryManager(embeddings)
  56. # 带工具的 LLM(健康知识库检索)
  57. async def health_retrieve(query: str, user_id: int) -> list[dict]:
  58. try:
  59. results = await retriever.retrieve(
  60. query,
  61. filters={"user_id": user_id},
  62. k=3,
  63. )
  64. return results
  65. except Exception as e:
  66. logger.warning("健康知识检索失败: %s", e)
  67. return []
  68. async def generate_answer(state: HealthButlerState) -> dict:
  69. """带健康知识检索的 LLM 生成"""
  70. messages = [SystemMessage(content=HEALTH_BUTLER_PROMPT)]
  71. # 家庭上下文
  72. ctx = state.get("context") or {}
  73. if ctx:
  74. child_info = []
  75. if ctx.get("child_id"):
  76. child_info.append(f"孩子ID: {ctx['child_id']}")
  77. if ctx.get("family_id"):
  78. child_info.append(f"家庭ID: {ctx['family_id']}")
  79. if child_info:
  80. messages.append(SystemMessage(
  81. content="用户背景信息: " + ", ".join(child_info)
  82. ))
  83. # 健康知识检索
  84. retrieved = await health_retrieve(state["query"], state["user_id"])
  85. sources = []
  86. if retrieved:
  87. context_texts = []
  88. for i, r in enumerate(retrieved[:3]):
  89. src = r.get("metadata", {})
  90. title = src.get("title", "健康知识")
  91. content = r["content"][:300]
  92. context_texts.append(f"[{i+1}] {title}:\n{content}")
  93. sources.append({
  94. "type": "knowledge",
  95. "title": title,
  96. "content": content,
  97. })
  98. knowledge_context = "\n\n---\n".join(context_texts)
  99. messages.append(SystemMessage(
  100. content=f"以下是与问题相关的健康知识参考:\n{knowledge_context}"
  101. ))
  102. # 长期记忆
  103. try:
  104. memories = await memory_mgr.recall(state["user_id"], state["query"])
  105. if memories:
  106. mem_text = "\n".join([f"- {m}" for m in memories[:3]])
  107. messages.append(SystemMessage(
  108. content=f"该用户的历史健康咨询记录:\n{mem_text}"
  109. ))
  110. except Exception as e:
  111. logger.warning("召回健康记忆失败: %s", e)
  112. # 用户问题
  113. messages.append(HumanMessage(content=state["query"]))
  114. response = await llm.ainvoke(messages)
  115. answer = response.content
  116. return {
  117. "answer": answer,
  118. "sources": sources,
  119. "memory_messages": [
  120. {"role": "user", "content": state["query"]},
  121. {"role": "assistant", "content": answer},
  122. ],
  123. }
  124. async def save_memory(state: HealthButlerState) -> dict:
  125. """保存对话到长期记忆"""
  126. try:
  127. if state.get("memory_messages"):
  128. await memory_mgr.save_conversation(
  129. state["user_id"],
  130. state.get("conversation_id") or f"health_{state['user_id']}",
  131. state["memory_messages"],
  132. )
  133. except Exception as e:
  134. logger.warning("保存健康管家记忆失败: %s", e)
  135. return {}
  136. # ── 构建图 ──
  137. builder = StateGraph(HealthButlerState)
  138. builder.add_node("generate_answer", generate_answer)
  139. builder.add_node("save_memory", save_memory)
  140. builder.add_edge(START, "generate_answer")
  141. builder.add_edge("generate_answer", "save_memory")
  142. builder.add_edge("save_memory", END)
  143. checkpointer = MemorySaver()
  144. graph = builder.compile(checkpointer=checkpointer)
  145. return graph