feat(backend): AI 服务层 — 千问流式 LLM + 月报解析 + 健康度计算 + 风险检测 + Copilot
- LLM 客户端:全部 SSE 流式输出,兼容 OpenAI 接口
- AI 月报解析:SSE 流式端点 POST /reports/{id}/parse
- 健康度计算引擎:四维评分(财务/经营/AI商业化/AI成本)
- 风险自动检测引擎:6 条规则自动检测指标越界
- AI Copilot:SSE 流式对话 POST /copilot/chat
- 权限中间件:角色级 + 字段级权限控制
- 测试:21 个新测试(健康度 8 + 风险检测 8 + 权限 5),总计 64 passed
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"""月报 AI 解析服务。
|
||||
|
||||
使用千问 LLM 从月报原始文本中提取:
|
||||
1. 结构化指标数据(营收、现金流、团队等)
|
||||
2. AI 摘要
|
||||
3. 关注点列表
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from app.services.llm_client import llm_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SYSTEM_PROMPT = """你是投后管理领域的专业分析师。请分析以下企业月报内容,提取结构化信息。
|
||||
|
||||
输出 JSON 格式如下:
|
||||
{
|
||||
"structured_data": {
|
||||
"revenue": {"value": "", "unit": "万元", "yoy_change": "", "note": ""},
|
||||
"cash_balance": {"value": "", "unit": "万元", "runway_months": 0, "note": ""},
|
||||
"burn_rate": {"value": "", "unit": "万元/月", "trend": "up/stable/down", "note": ""},
|
||||
"headcount": {"total": 0, "new_hires": 0, "departures": 0, "note": ""},
|
||||
"key_metrics": [{"name": "", "value": "", "change": "", "note": ""}]
|
||||
},
|
||||
"ai_summary": "一段 100-200 字的月报摘要,概括企业经营状况和关键变化",
|
||||
"ai_concerns": {
|
||||
"items": [
|
||||
{"category": "financial/operational/org/ai_specific", "severity": "low/medium/high", "description": ""}
|
||||
],
|
||||
"highlights": ["本期亮点1", "本期亮点2"]
|
||||
}
|
||||
}
|
||||
|
||||
注意:
|
||||
- 如果月报内容不足以提取某项指标,对应字段留空或为 0
|
||||
- severity 只能是 low/medium/high
|
||||
- category 只能是 financial/operational/org/ai_specific
|
||||
- 严格输出 JSON,不要包含其他文字"""
|
||||
|
||||
USER_PROMPT_TEMPLATE = """请分析以下 {period} 月报内容:
|
||||
|
||||
<<<USER_INPUT>>>
|
||||
{content}
|
||||
<<<END_USER_INPUT>>>
|
||||
"""
|
||||
|
||||
|
||||
async def parse_report(
|
||||
content: str,
|
||||
period_year: int,
|
||||
period_month: int,
|
||||
) -> dict[str, Any]:
|
||||
"""解析月报内容,返回结构化数据。
|
||||
|
||||
Args:
|
||||
content: 月报原始文本
|
||||
period_year: 报告年份
|
||||
period_month: 报告月份
|
||||
|
||||
Returns:
|
||||
包含 structured_data / ai_summary / ai_concerns 的字典
|
||||
|
||||
Raises:
|
||||
RuntimeError: LLM 调用失败
|
||||
"""
|
||||
if not content or not content.strip():
|
||||
return {
|
||||
"structured_data": {},
|
||||
"ai_summary": "月报内容为空",
|
||||
"ai_concerns": {"items": [], "highlights": []},
|
||||
}
|
||||
|
||||
user_prompt = USER_PROMPT_TEMPLATE.format(
|
||||
period=f"{period_year}年{period_month}月",
|
||||
content=content[:4000], # 限制输入长度
|
||||
)
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_prompt},
|
||||
]
|
||||
|
||||
try:
|
||||
result = await llm_client.chat_json(messages, temperature=0.3, max_tokens=2000)
|
||||
logger.info("月报 AI 解析成功: period=%s-%s", period_year, period_month)
|
||||
return result
|
||||
except RuntimeError as e:
|
||||
logger.error("月报 AI 解析失败: %s", e)
|
||||
# 降级:返回空结构
|
||||
return {
|
||||
"structured_data": {},
|
||||
"ai_summary": f"AI 解析失败: {e}",
|
||||
"ai_concerns": {"items": [], "highlights": []},
|
||||
"fallback_used": True,
|
||||
}
|
||||
Reference in New Issue
Block a user