stock-ai-agent/backend/app/agent/question_analyzer.py
2026-02-03 23:56:50 +08:00

312 lines
11 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.

"""
问题分析器 - 使用LLM深度理解用户意图
"""
import json
import asyncio
from typing import Dict, Any, Optional, List
from app.services.llm_service import llm_service
from app.utils.logger import logger
class QuestionAnalyzer:
"""智能问题分析器 - 使用LLM深度理解用户意图"""
def __init__(self):
"""初始化问题分析器"""
self.use_llm = llm_service.client is not None
if not self.use_llm:
logger.warning("LLM未配置QuestionAnalyzer将使用降级模式")
async def analyze_question(
self,
question: str,
context: List[Dict],
session_id: str
) -> Dict[str, Any]:
"""
深度分析用户问题
Args:
question: 用户问题
context: 对话历史上下文
session_id: 会话ID
Returns:
QuestionIntent: {
'type': 'stock_analysis' | 'market_overview' | 'knowledge' | 'chat',
'target': {
'stock_code': str,
'stock_name': str,
'market': 'A股' | '美股'
},
'dimensions': {
'price_trend': bool, # 价格走势
'technical': bool, # 技术指标
'fundamental': bool, # 基本面
'valuation': bool, # 估值
'money_flow': bool, # 资金流向
'risk': bool # 风险分析
},
'time_scope': {
'short_term': bool, # 短期(1-2周)
'medium_term': bool, # 中期(1-3月)
'long_term': bool # 长期(半年+)
},
'analysis_depth': 'quick' | 'standard' | 'deep',
'specific_concerns': List[str], # 特定关注点
'context_references': {
'refers_to_previous': bool,
'comparison_target': str | None
},
'user_style': {
'tone': 'professional' | 'casual',
'detail_level': 'brief' | 'detailed'
}
}
"""
if not self.use_llm:
# 降级模式:返回基本的意图分析
return self._fallback_analysis(question)
# 构建上下文字符串
context_str = self._format_context(context)
# 构建LLM prompt
prompt = self._build_analysis_prompt(question, context_str)
try:
# 异步调用LLM
result = await self._call_llm_async(
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
max_tokens=800
)
if not result:
logger.warning("LLM返回空结果使用降级模式")
return self._fallback_analysis(question)
# 清理和解析JSON
intent = self._parse_llm_response(result)
if intent:
logger.info(f"问题分析成功: type={intent.get('type')}, dimensions={intent.get('dimensions')}")
return intent
else:
logger.warning("JSON解析失败使用降级模式")
return self._fallback_analysis(question)
except Exception as e:
logger.error(f"问题分析失败: {e}")
return self._fallback_analysis(question)
async def _call_llm_async(
self,
messages: List[Dict[str, str]],
temperature: float = 0.3,
max_tokens: int = 800
) -> Optional[str]:
"""异步调用LLM"""
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
lambda: llm_service.chat(messages, temperature, max_tokens)
)
def _format_context(self, context: List[Dict]) -> str:
"""格式化对话历史上下文"""
if not context:
return ""
context_str = "\n\n【对话历史】\n"
# 只取最近4条消息
for msg in context[-4:]:
role = "用户" if msg["role"] == "user" else "助手"
content = msg['content'][:100] # 限制长度
context_str += f"{role}: {content}\n"
return context_str
def _build_analysis_prompt(self, question: str, context_str: str) -> str:
"""构建问题分析的LLM prompt"""
prompt = f"""你是一个专业的金融问题分析专家。请深度分析用户的问题,提取结构化信息。
{context_str}
【当前问题】
用户: {question}
请分析以下维度:
1. **问题类型**
- stock_analysis: 针对**特定单只股票**的分析(如"贵州茅台怎么样""分析比亚迪""AAPL走势"
**注意**:如果用户问的是"板块""行业""概念股"这不是stock_analysis而是market_overview
- market_overview: 市场整体分析、行业板块分析、投资机会(如"最近有什么投资机会""商业航天板块怎么样""新能源行业走势""现在适合买股票吗"
- knowledge: 金融知识问答(如"什么是MACD""如何看K线图"
- chat: 一般对话(如"你好""在吗"
**重要**判断是stock_analysis还是market_overview的关键
- 如果提到具体的公司名称或股票代码 → stock_analysis
- 如果提到"板块""行业""概念""赛道""领域" → market_overview
- 如果问"哪些股票""什么机会" → market_overview
2. **用户关注维度**如果是stock_analysis
分析用户想了解哪些方面:
- price_trend: 价格走势、涨跌情况、最新价格
- technical: 技术指标MACD、RSI、均线、KDJ等
- fundamental: 基本面(公司业务、行业地位、财务状况)
- valuation: 估值水平PE、PB、市值、估值是否合理
- money_flow: 资金流向、主力动向、大单流入流出
- risk: 风险分析、风险提示、投资风险
3. **时间范围**
- short_term: 短期1-2周- 如"短期走势""近期表现"
- medium_term: 中期1-3月- 如"中期趋势""未来一个月"
- long_term: 长期(半年以上)- 如"长期投资""适合长期持有吗"
4. **分析深度**
- quick: 快速查看(只需要基本信息,如"价格多少"
- standard: 标准分析(常规分析,如"怎么样""分析一下"
- deep: 深度分析(全面详细,如"全面分析""深度研究"
5. **特定关注点**
提取用户明确提到的关注点,如:
- "支撑位在哪"
- "盈利能力如何"
- "适合长期持有吗"
- "有没有金叉"
6. **上下文引用**
- 是否引用了之前的对话("这只股票""""那技术面呢"
- 是否要求对比分析("和上次相比""对比一下"
7. **用户风格**
- tone: professional专业使用专业术语/ casual随意通俗易懂
- detail_level: brief简洁简短回答/ detailed详细详细分析
请以JSON格式返回分析结果
{{
"type": "问题类型",
"target": {{
"stock_code": "股票代码如有只返回纯数字代码如600519或002594不要包含市场标识",
"stock_name": "股票名称(如有,只返回公司名称,如贵州茅台或比亚迪)",
"market": "A股/美股"
}},
"dimensions": {{
"price_trend": true/false,
"technical": true/false,
"fundamental": true/false,
"valuation": true/false,
"money_flow": true/false,
"risk": true/false
}},
"time_scope": {{
"short_term": true/false,
"medium_term": true/false,
"long_term": true/false
}},
"analysis_depth": "quick/standard/deep",
"specific_concerns": ["关注点1", "关注点2"],
"context_references": {{
"refers_to_previous": true/false,
"comparison_target": "对比目标(如有)"
}},
"user_style": {{
"tone": "professional/casual",
"detail_level": "brief/detailed"
}}
}}
只返回JSON不要有任何其他内容。"""
return prompt
def _parse_llm_response(self, response: str) -> Optional[Dict[str, Any]]:
"""解析LLM返回的JSON响应"""
try:
# 清理结果移除可能的markdown代码块标记
result = response.strip()
if result.startswith("```json"):
result = result[7:]
if result.startswith("```"):
result = result[3:]
if result.endswith("```"):
result = result[:-3]
result = result.strip()
# 检查是否为空
if not result:
return None
# 解析JSON
intent = json.loads(result)
return intent
except json.JSONDecodeError as e:
logger.error(f"JSON解析失败: {e}, 原始响应: {response[:200]}")
return None
except Exception as e:
logger.error(f"解析LLM响应失败: {e}")
return None
def _fallback_analysis(self, question: str) -> Dict[str, Any]:
"""降级模式:基于规则的简单分析"""
question_lower = question.lower()
# 简单的关键词匹配
is_stock_query = any(kw in question for kw in [
"股票", "分析", "怎么样", "如何", "走势", "价格", "", ""
])
if is_stock_query:
# 尝试提取股票名称(简单规则)
return {
'type': 'stock_analysis',
'target': {
'stock_code': '',
'stock_name': '',
'market': 'A股'
},
'dimensions': {
'price_trend': True,
'technical': True,
'fundamental': True,
'valuation': False,
'money_flow': False,
'risk': False
},
'time_scope': {
'short_term': True,
'medium_term': True,
'long_term': False
},
'analysis_depth': 'standard',
'specific_concerns': [],
'context_references': {
'refers_to_previous': False,
'comparison_target': None
},
'user_style': {
'tone': 'casual',
'detail_level': 'detailed'
}
}
else:
# 默认为一般对话
return {
'type': 'chat',
'target': {},
'dimensions': {},
'time_scope': {},
'analysis_depth': 'quick',
'specific_concerns': [],
'context_references': {
'refers_to_previous': False,
'comparison_target': None
},
'user_style': {
'tone': 'casual',
'detail_level': 'brief'
}
}