Files
muyingbaogaojiedu/后端-孕期报告解读-api/app/services/ai_service.py
shutiaozhao127-boop cfdeeb5e41 chore: 初始化项目结构并添加基础配置文件
添加后端API和前端项目的初始目录结构,包括应用层模块、工具模块和模型定义
配置前后端的.dockerignore文件以排除开发环境文件
添加前端样式文件,包括字体、Tailwind CSS和主题配置
初始化Python虚拟环境中的类型标记文件
2026-04-01 16:55:24 +08:00

240 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
AI service: Report analysis, chat, doctor questions.
Uses OpenAI-compatible API (e.g. SiliconFlow).
"""
import json
import logging
from typing import Optional
from openai import AsyncOpenAI
from app.config import settings
from app.models.report import Report
from app.models.user import User
logger = logging.getLogger(__name__)
def _get_client() -> AsyncOpenAI:
return AsyncOpenAI(api_key=settings.ai_api_key, base_url=settings.ai_base_url)
def _build_report_context(report: Report, user: User) -> str:
"""构建报告上下文给 AI"""
lines = [
f"孕妈信息: 年龄{user.age}岁",
]
if report.pregnancy_week:
lines.append(f"检查时孕周: 第{report.pregnancy_week}周")
if report.report_date:
lines.append(f"报告日期: {report.report_date}")
if user.last_period_date:
lines.append(f"末次月经: {user.last_period_date}")
if user.height:
lines.append(f"身高: {user.height}cm")
if user.current_weight:
lines.append(f"当前体重: {user.current_weight}kg")
if user.blood_type:
lines.append(f"血型: {user.blood_type}")
if user.medical_history:
lines.append(f"病史: {', '.join(user.medical_history)}")
lines.append(f"\n报告类型: {report.report_type}")
lines.append("\n检查指标:")
for item in report.data_items:
ref = f", 参考范围: {item.reference_range}" if item.reference_range else ""
lines.append(f" - {item.name}: {item.value} {item.unit}{ref}")
if report.symptoms:
lines.append(f"\n自觉症状: {', '.join(report.symptoms)}")
return "\n".join(lines)
def _parse_json_response(text: str) -> dict:
"""从 AI 回复中提取 JSON"""
if "```json" in text:
text = text.split("```json")[1].split("```")[0]
elif "```" in text:
text = text.split("```")[1].split("```")[0]
return json.loads(text.strip())
async def _chat_complete(system: str, user_content: str, messages: Optional[list] = None, max_tokens: int = 4096) -> str:
"""统一调用 AI 文本模型"""
client = _get_client()
msgs = [{"role": "system", "content": system}]
if messages:
msgs.extend(messages)
else:
msgs.append({"role": "user", "content": user_content})
try:
response = await client.chat.completions.create(
model=settings.ai_text_model,
max_tokens=max_tokens,
temperature=0.7, # 降低温度提高生成速度
messages=msgs,
)
except Exception as e:
err_msg = str(e)
logger.error("AI 调用失败: %s: %s", type(e).__name__, err_msg[:200])
if "auth" in err_msg.lower() or "api key" in err_msg.lower():
raise RuntimeError("AI 服务认证失败,请检查 API Key") from e
raise RuntimeError("AI 服务暂时不可用,请稍后重试") from e
return response.choices[0].message.content
# ─── Report Analysis ───
ANALYSIS_SYSTEM_PROMPT = """你是一位专业、温暖的孕期健康顾问AI。你的任务是解读孕期检查报告,用通俗易懂的语言帮助孕妈理解检查结果。
核心要求:
1. 必须结合孕周来判断指标是否正常(孕期不同阶段的正常范围不同,如血红蛋白在孕中晚期会生理性下降)
2. 将每个指标的测定结果与报告中的参考范围对比,准确判断偏高/偏低/正常
3. 如果参考范围缺失,根据你的医学知识和该孕周的标准范围来判断
4. 用温暖、关怀的语气,避免医学术语恐吓
5. 对每个指标给出白话化的昵称(如血红蛋白→"氧气快递员",双顶径→"宝宝头围",腹围→"宝宝肚围",股骨长→"宝宝腿长")
6. 解释每个指标的含义、对宝宝的影响、行动建议
7. 给出整体评估和风险等级
8. 行动建议按优先级排序
**特别注意:**
- 如果是B超报告,重点关注胎儿生长指标(双顶径、腹围、股骨长),即使只有这3个指标也要给出完整的解读
- B超报告的解读要重点说明宝宝的生长发育情况,是否符合孕周标准
- 即使指标数量很少(如只有3个),也要给出详细的解读和建议
请严格按以下JSON格式返回:
{
"overall_status": "normal/attention/warning",
"risk_level": "low/medium/high",
"overall_summary": "整体评估文案",
"emotional_summary": "情感化总结,如'宝宝发育良好,继续保持'",
"categories": [
{
"category": "分类名称",
"status": "normal/attention/warning",
"indicators": [
{
"name": "指标名",
"nickname": "白话昵称",
"value": "值",
"status": "normal/high/low",
"what_is_it": "通俗解释",
"impact": "对宝宝的影响",
"suggestion": "行动建议",
"recheck_reminder": "复查提醒(可选)"
}
]
}
],
"action_items": [
{
"content": "具体建议",
"priority": "high/medium/low",
"category": "饮食/运动/就医/复查"
}
],
"tips": ["温馨提示1", "温馨提示2"]
}"""
async def analyze_report(report: Report, user: User) -> dict:
"""调用 AI 解读报告"""
logger.info("开始AI解读报告 report_id=%s", report.id)
context = _build_report_context(report, user)
response_text = await _chat_complete(
system=ANALYSIS_SYSTEM_PROMPT,
user_content=f"请解读以下孕期检查报告:\n\n{context}",
)
try:
return _parse_json_response(response_text)
except (json.JSONDecodeError, IndexError):
return {
"overall_status": "normal",
"risk_level": "low",
"overall_summary": response_text,
"emotional_summary": "报告解读完成",
"categories": [],
"action_items": [],
"tips": [],
}
# ─── Chat ───
CHAT_SYSTEM_PROMPT = """你是一位专业、温暖的孕期健康顾问AI。孕妈正在就她的检查报告向你提问。
要求:
1. 用通俗易懂、温暖关怀的语气回答
2. 基于报告数据给出专业建议
3. 遇到严重问题建议及时就医
4. 回答简洁明了,避免过长
以下是孕妈的检查报告信息:
{report_context}"""
async def chat_with_ai(
report: Report, user: User, messages: list[dict], user_message: str
) -> str:
"""AI 对话追问"""
context = _build_report_context(report, user)
system = CHAT_SYSTEM_PROMPT.format(report_context=context)
conversation = [{"role": "system", "content": system}]
for msg in messages[-10:]:
conversation.append({"role": msg["role"], "content": msg["content"]})
conversation.append({"role": "user", "content": user_message})
client = _get_client()
try:
response = await client.chat.completions.create(
model=settings.ai_text_model,
max_tokens=1024,
messages=conversation,
)
except Exception as e:
logger.error("AI 对话失败: %s", e)
raise RuntimeError("AI 对话服务暂时不可用") from e
return response.choices[0].message.content
# ─── Doctor Questions ───
DOCTOR_QUESTIONS_SYSTEM_PROMPT = """你是一位孕期健康顾问AI。根据检查报告中的异常项,帮孕妈生成3-5条看医生时可以问的问题。
要求:
1. 问题要专业但容易理解
2. 按重要性排序
3. 给出为什么要问这个问题的原因
请严格按以下JSON格式返回:
{
"questions": [
{
"category": "分类",
"question": "问医生的话术",
"reason": "为什么要问这个",
"priority": "high/medium/low"
}
]
}"""
async def generate_doctor_questions(report: Report, user: User) -> dict:
"""生成问诊清单"""
context = _build_report_context(report, user)
response_text = await _chat_complete(
system=DOCTOR_QUESTIONS_SYSTEM_PROMPT,
user_content=f"请根据以下报告生成问诊清单:\n\n{context}",
max_tokens=1024,
)
try:
return _parse_json_response(response_text)
except (json.JSONDecodeError, IndexError):
return {"questions": []}