| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- """
- AI 健康教练 LangGraph - 专为儿童健康咨询设计的对话图
- Uses RAG retrieval from health knowledge base.
- """
- from typing import TypedDict
- from langgraph.graph import StateGraph, START, END
- from langgraph.checkpoint.memory import MemorySaver
- from langchain_openai import ChatOpenAI, OpenAIEmbeddings
- from langchain_core.messages import SystemMessage, HumanMessage
- from app.rag.retriever import RagRetriever
- from app.memory.store import MemoryManager
- from app.config import settings
- import logging
- logger = logging.getLogger(__name__)
- class HealthCoachState(TypedDict):
- query: str
- user_id: int
- child_id: int | None
- conversation_id: str | None
- context: dict | None
- answer: str | None
- sources: list[dict]
- memory_messages: list | None
- HEALTH_COACH_PROMPT = """你是一个儿童健康教练,喜欢用提问的方式引导家长发现问题和解决方案。
- 核心原则:
- 1. 先问后答:不要直接给答案,用问题引导家长思考
- - "孩子这种情况多久了?"、"平时饮食习惯是怎样的?"、"体重身高发育曲线有没有记录?"
- 2. 用开放式问题了解情况,再给针对性建议
- 3. 语气温暖,像一个耐心的教练在陪伴家长成长
- 你的职责:
- 1. 儿童体质调理(如挑食、瘦弱、易疲劳、反复生病)—— 用提问了解情况
- 2. 科学喂养和营养建议 —— 先问饮食现状再给建议
- 3. 日常保健(睡眠、运动、季节防护)—— 引导家长发现生活中的问题
- 4. 体质辨识基础咨询 —— 通过提问帮助家长理解孩子体质特点
- 5. 健康问题判断 —— 先收集症状信息再建议是否就医
- 回答结构(教练风格):
- 1. 提问:先问1-2个关键问题了解情况
- 2. 分析:简短总结你观察到的问题
- 3. 建议:给出2-3个可操作的建议
- 4. 跟进问句:留一个后续跟进的问题,体现教练陪伴感
- 原则:
- - 涉及严重症状(急性腹痛、高烧不退、严重外伤等)立刻建议就医,不提问
- - 不确定时明确说"建议咨询专业医生"
- - 始终用中文回答
- """
- def create_health_coach_graph():
- """创建AI健康教练 StateGraph"""
- # LLM
- llm = ChatOpenAI(
- model=settings.llm_model,
- api_key=settings.llm_api_key,
- base_url=settings.llm_base_url,
- temperature=0.7,
- )
- # 嵌入和检索
- embeddings = OpenAIEmbeddings(
- model=settings.embedding_model,
- api_key=settings.effective_embedding_api_key,
- base_url=settings.effective_embedding_base_url,
- )
- retriever = RagRetriever(collection_name="health_knowledge")
- memory_mgr = MemoryManager(embeddings)
- # 带工具的 LLM(健康知识库检索)
- async def health_retrieve(query: str, user_id: int) -> list[dict]:
- try:
- results = await retriever.retrieve(
- query,
- filters={"user_id": user_id},
- k=3,
- )
- return results
- except Exception as e:
- logger.warning("健康知识检索失败: %s", e)
- return []
- async def generate_answer(state: HealthCoachState) -> dict:
- """带健康知识检索的 LLM 生成"""
- messages = [SystemMessage(content=HEALTH_BUTLER_PROMPT)]
- # 家庭上下文
- ctx = state.get("context") or {}
- if ctx:
- child_info = []
- if ctx.get("child_id"):
- child_info.append(f"孩子ID: {ctx['child_id']}")
- if ctx.get("family_id"):
- child_info.append(f"家庭ID: {ctx['family_id']}")
- if child_info:
- messages.append(SystemMessage(
- content="用户背景信息: " + ", ".join(child_info)
- ))
- # 健康知识检索
- retrieved = await health_retrieve(state["query"], state["user_id"])
- sources = []
- if retrieved:
- context_texts = []
- for i, r in enumerate(retrieved[:3]):
- src = r.get("metadata", {})
- title = src.get("title", "健康知识")
- content = r["content"][:300]
- context_texts.append(f"[{i+1}] {title}:\n{content}")
- sources.append({
- "type": "knowledge",
- "title": title,
- "content": content,
- })
- knowledge_context = "\n\n---\n".join(context_texts)
- messages.append(SystemMessage(
- content=f"以下是与问题相关的健康知识参考:\n{knowledge_context}"
- ))
- # 长期记忆
- try:
- memories = await memory_mgr.recall(state["user_id"], state["query"])
- if memories:
- mem_text = "\n".join([f"- {m}" for m in memories[:3]])
- messages.append(SystemMessage(
- content=f"该用户的历史健康咨询记录:\n{mem_text}"
- ))
- except Exception as e:
- logger.warning("召回健康记忆失败: %s", e)
- # 用户问题
- messages.append(HumanMessage(content=state["query"]))
- response = await llm.ainvoke(messages)
- answer = response.content
- return {
- "answer": answer,
- "sources": sources,
- "memory_messages": [
- {"role": "user", "content": state["query"]},
- {"role": "assistant", "content": answer},
- ],
- }
- async def save_memory(state: HealthCoachState) -> dict:
- """保存对话到长期记忆"""
- try:
- if state.get("memory_messages"):
- await memory_mgr.save_conversation(
- state["user_id"],
- state.get("conversation_id") or f"health_{state['user_id']}",
- state["memory_messages"],
- )
- except Exception as e:
- logger.warning("保存健康教练记忆失败: %s", e)
- return {}
- # ── 构建图 ──
- builder = StateGraph(HealthCoachState)
- builder.add_node("generate_answer", generate_answer)
- builder.add_node("save_memory", save_memory)
- builder.add_edge(START, "generate_answer")
- builder.add_edge("generate_answer", "save_memory")
- builder.add_edge("save_memory", END)
- checkpointer = MemorySaver()
- graph = builder.compile(checkpointer=checkpointer)
- return graph
|