967 lines
32 KiB
Python
967 lines
32 KiB
Python
"""
|
||
智能Agent - 真正使用LLM进行全面分析
|
||
"""
|
||
import re
|
||
import json
|
||
from typing import Dict, Any, Optional, List
|
||
from app.config import get_settings
|
||
from app.agent.context import ContextManager
|
||
from app.agent.skill_manager import skill_manager
|
||
from app.skills.market_data import MarketDataSkill
|
||
from app.skills.technical_analysis import TechnicalAnalysisSkill
|
||
from app.skills.fundamental import FundamentalSkill
|
||
from app.skills.visualization import VisualizationSkill
|
||
from app.skills.brave_search import BraveSearchSkill
|
||
from app.services.llm_service import llm_service
|
||
from app.services.tushare_service import tushare_service
|
||
from app.utils.logger import logger
|
||
|
||
|
||
class SmartStockAgent:
|
||
"""智能股票分析Agent - 深度集成LLM"""
|
||
|
||
def __init__(self):
|
||
"""初始化Agent"""
|
||
self.context_manager = ContextManager()
|
||
self.settings = get_settings()
|
||
|
||
# 注册技能
|
||
self._register_skills()
|
||
|
||
# 检查LLM是否可用
|
||
self.use_llm = bool(self.settings.zhipuai_api_key) and llm_service.client is not None
|
||
|
||
if self.use_llm:
|
||
logger.info("Smart Agent初始化完成(LLM深度集成模式 + Brave搜索)")
|
||
else:
|
||
logger.warning("Smart Agent初始化完成(规则模式,建议配置LLM)")
|
||
|
||
def _register_skills(self):
|
||
"""注册所有技能"""
|
||
skill_manager.register(MarketDataSkill())
|
||
skill_manager.register(TechnicalAnalysisSkill())
|
||
skill_manager.register(FundamentalSkill())
|
||
skill_manager.register(VisualizationSkill())
|
||
skill_manager.register(BraveSearchSkill())
|
||
logger.info("技能注册完成(包含Brave搜索)")
|
||
|
||
async def process_message(
|
||
self,
|
||
message: str,
|
||
session_id: str,
|
||
user_id: Optional[str] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
处理用户消息(智能版)
|
||
|
||
Args:
|
||
message: 用户消息
|
||
session_id: 会话ID
|
||
user_id: 用户ID
|
||
|
||
Returns:
|
||
响应结果
|
||
"""
|
||
logger.info(f"处理消息: {message[:50]}...")
|
||
|
||
# 保存用户消息
|
||
self.context_manager.add_message(session_id, "user", message)
|
||
|
||
# 第一步:使用LLM理解问题意图
|
||
intent_analysis = await self._analyze_question_intent(message)
|
||
|
||
if not intent_analysis:
|
||
response = {
|
||
"message": "抱歉,我无法理解您的问题。请重新描述您的需求。",
|
||
"metadata": {"type": "error"}
|
||
}
|
||
self.context_manager.add_message(session_id, "assistant", response["message"])
|
||
return response
|
||
|
||
# 第二步:根据意图类型处理
|
||
question_type = intent_analysis['type']
|
||
|
||
if question_type == 'stock_specific':
|
||
# 针对特定股票的问题
|
||
response = await self._handle_stock_question(intent_analysis, message)
|
||
elif question_type == 'macro_finance':
|
||
# 宏观金融问题
|
||
response = await self._handle_macro_question(intent_analysis, message)
|
||
elif question_type == 'knowledge':
|
||
# 金融知识问答
|
||
response = await self._handle_knowledge_question(intent_analysis, message)
|
||
else:
|
||
response = {
|
||
"message": "抱歉,我暂时无法处理这类问题。",
|
||
"metadata": {"type": "error"}
|
||
}
|
||
|
||
# 保存助手响应
|
||
self.context_manager.add_message(
|
||
session_id,
|
||
"assistant",
|
||
response["message"],
|
||
metadata=response.get("metadata")
|
||
)
|
||
|
||
return response
|
||
|
||
def _is_comprehensive_analysis(self, message: str) -> bool:
|
||
"""
|
||
判断是否需要全面分析
|
||
|
||
默认情况下,如果用户只是简单提到股票名称或代码,就进行全面分析
|
||
只有明确要求特定信息时(如"技术指标"、"K线图"等),才做单一查询
|
||
"""
|
||
# 明确要求单一查询的关键词
|
||
single_query_keywords = [
|
||
"k线", "图表", "走势图", "kline",
|
||
"技术指标", "macd", "rsi", "均线", "kdj",
|
||
"基本面", "公司信息", "行业",
|
||
"实时行情", "价格", "涨跌"
|
||
]
|
||
|
||
# 如果明确要求单一查询,返回False
|
||
if any(keyword in message.lower() for keyword in single_query_keywords):
|
||
return False
|
||
|
||
# 默认进行全面分析
|
||
return True
|
||
|
||
async def _comprehensive_analysis(
|
||
self,
|
||
stock_code: str,
|
||
stock_name: Optional[str],
|
||
message: str
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
全面分析:整合多个数据源 + LLM深度分析
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
stock_name: 股票名称
|
||
message: 用户消息
|
||
|
||
Returns:
|
||
综合分析结果
|
||
"""
|
||
logger.info(f"执行全面分析: {stock_code}")
|
||
|
||
display_name = stock_name or stock_code
|
||
|
||
# 1. 并行获取所有数据
|
||
try:
|
||
# 获取实时行情
|
||
quote_result = await skill_manager.execute_skill(
|
||
"market_data",
|
||
stock_code=stock_code,
|
||
data_type="quote"
|
||
)
|
||
|
||
# 获取技术指标
|
||
technical_result = await skill_manager.execute_skill(
|
||
"technical_analysis",
|
||
stock_code=stock_code,
|
||
indicators=["ma", "macd", "rsi", "kdj"]
|
||
)
|
||
|
||
# 获取基本面
|
||
fundamental_result = await skill_manager.execute_skill(
|
||
"fundamental",
|
||
stock_code=stock_code
|
||
)
|
||
|
||
# 获取最新新闻(Brave搜索)
|
||
search_query = f"{display_name} {stock_code} 股票 最新消息"
|
||
news_result = await skill_manager.execute_skill(
|
||
"brave_search",
|
||
query=search_query,
|
||
search_type="news",
|
||
count=5,
|
||
freshness="pw" # 过去一周
|
||
)
|
||
|
||
# 整合数据
|
||
all_data = {
|
||
"stock_code": stock_code,
|
||
"stock_name": display_name,
|
||
"quote": quote_result.get("data") if quote_result.get("success") else None,
|
||
"technical": technical_result.get("data") if technical_result.get("success") else None,
|
||
"fundamental": fundamental_result.get("data") if fundamental_result.get("success") else None,
|
||
"news": news_result.get("results") if news_result and not news_result.get("error") else None
|
||
}
|
||
|
||
# 2. 使用LLM进行深度分析
|
||
if self.use_llm:
|
||
analysis = await self._llm_comprehensive_analysis(all_data, message)
|
||
else:
|
||
analysis = self._rule_based_analysis(all_data)
|
||
|
||
return {
|
||
"message": analysis,
|
||
"metadata": {
|
||
"type": "comprehensive",
|
||
"data": all_data
|
||
}
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"全面分析失败: {e}")
|
||
return {
|
||
"message": f"分析{display_name}时出错:{str(e)}",
|
||
"metadata": {"type": "error"}
|
||
}
|
||
|
||
async def _llm_comprehensive_analysis(
|
||
self,
|
||
data: Dict[str, Any],
|
||
user_message: str
|
||
) -> str:
|
||
"""使用LLM进行深度综合分析"""
|
||
|
||
# 获取当前时间
|
||
from datetime import datetime
|
||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||
|
||
# 获取行情数据的交易日期
|
||
quote_date = "未知"
|
||
if data.get('quote') and data['quote'].get('trade_date'):
|
||
quote_date = data['quote']['trade_date']
|
||
|
||
# 构建新闻摘要
|
||
news_summary = ""
|
||
news_source_info = ""
|
||
if data.get('news'):
|
||
news_summary = "\n【消息面分析】\n"
|
||
news_summary += f"数据来源:Brave Search API\n"
|
||
news_summary += f"搜索时间:{current_time}\n"
|
||
news_summary += f"新闻范围:过去一周内相关新闻\n\n"
|
||
for idx, news_item in enumerate(data['news'][:5], 1):
|
||
news_summary += f"{idx}. {news_item.get('title', '无标题')}\n"
|
||
news_summary += f" 来源: {news_item.get('source', '未知')}\n"
|
||
news_summary += f" 摘要: {news_item.get('description', '无描述')}\n"
|
||
news_summary += f" 发布时间: {news_item.get('published', '未知')}\n\n"
|
||
news_source_info = "(消息来源:Brave搜索引擎,数据可能存在延迟)"
|
||
else:
|
||
news_summary = "\n【消息面分析】\n暂无最新新闻数据\n"
|
||
|
||
# 构建详细的分析提示
|
||
prompt = f"""你是一位专业的股票分析师。请对{data['stock_name']}({data['stock_code']})进行全面分析,用简洁专业但易懂的语言回答。
|
||
|
||
用户问题:{user_message}
|
||
|
||
【实时行情数据】
|
||
数据来源:Tushare Pro API
|
||
交易日期:{quote_date}
|
||
{json.dumps(data.get('quote'), ensure_ascii=False, indent=2) if data.get('quote') else '数据获取失败'}
|
||
|
||
【技术指标数据】
|
||
数据来源:Tushare Pro API(基于历史K线数据计算)
|
||
计算截止日期:{quote_date}
|
||
{json.dumps(data.get('technical'), ensure_ascii=False, indent=2) if data.get('technical') else '数据获取失败'}
|
||
|
||
【基本面数据】
|
||
数据来源:Tushare Pro API
|
||
{json.dumps(data.get('fundamental'), ensure_ascii=False, indent=2) if data.get('fundamental') else '数据获取失败'}
|
||
{news_summary}
|
||
|
||
请按以下结构进行分析,并在每个部分明确标注数据来源和时效性:
|
||
|
||
## 一、基本面分析
|
||
分段说明公司情况,每个要点独立成段:
|
||
- 第一段:公司主营业务和行业地位
|
||
- 第二段:所属行业发展前景
|
||
- 第三段:如果有新闻,简要分析对公司的影响{news_source_info}
|
||
|
||
## 二、技术面分析(数据截止:{quote_date})
|
||
使用清晰的分段结构,每个技术指标独立成段:
|
||
|
||
**价格走势**
|
||
当前价格走势特征(上涨/下跌/震荡),结合成交量分析。
|
||
|
||
**均线系统**
|
||
短期均线(MA5、MA10)与长期均线(MA20、MA60)的位置关系,判断当前趋势(多头/空头/震荡)。
|
||
|
||
**MACD指标**
|
||
DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信号。
|
||
|
||
**RSI指标**
|
||
当前RSI值的位置,是否超买(>70)或超卖(<30),短期走势预判。
|
||
|
||
**支撑与压力**
|
||
关键支撑位和压力位的具体价格区间。
|
||
|
||
## 三、市场情绪分析
|
||
分段分析市场情绪:
|
||
- 第一段:当前市场情绪(乐观/谨慎/悲观)及原因
|
||
- 第二段:如果有新闻,分析是利好还是利空
|
||
- 第三段:短期可能的催化因素
|
||
|
||
## 四、投资建议
|
||
清晰分段,每个时间维度独立:
|
||
|
||
**短期(1-2周)**
|
||
明确的操作建议(买入/持有/观望/减仓)及理由。
|
||
|
||
**中期(1-3个月)**
|
||
趋势判断和策略建议。
|
||
|
||
**长期(半年以上)**
|
||
投资价值评估。
|
||
|
||
**风险提示**
|
||
主要风险点和注意事项。
|
||
|
||
## 五、总结
|
||
用一句话概括核心观点。
|
||
|
||
---
|
||
**数据说明**
|
||
- 行情数据来源:Tushare Pro(截止{quote_date})
|
||
- 技术指标:基于历史K线数据计算(截止{quote_date})
|
||
- 新闻数据:Brave搜索(搜索时间{current_time},范围:过去一周)
|
||
|
||
写作要求:
|
||
1. 语言简洁专业,避免过度修饰和比喻
|
||
2. 专业术语后用括号简单解释,例如"RSI超买(指标>70,股价可能回调)"
|
||
3. **重要:每个分析点必须独立成段,段落之间用空行分隔**
|
||
4. **技术面分析部分,每个指标必须使用加粗标题(**标题**)并独立成段**
|
||
5. 分析要客观理性,基于数据而非情绪
|
||
3. 分析要客观理性,基于数据而非情绪
|
||
4. 结论要明确,不要模棱两可
|
||
5. 控制在500-600字
|
||
6. 最后必须声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
|
||
"""
|
||
|
||
try:
|
||
analysis = llm_service.chat(
|
||
messages=[{"role": "user", "content": prompt}],
|
||
temperature=0.7,
|
||
max_tokens=2000
|
||
)
|
||
|
||
if analysis:
|
||
return f"【{data['stock_name']}({data['stock_code']}) - AI深度分析】\n\n{analysis}"
|
||
else:
|
||
return self._rule_based_analysis(data)
|
||
|
||
except Exception as e:
|
||
logger.error(f"LLM分析失败: {e}")
|
||
return self._rule_based_analysis(data)
|
||
|
||
async def _llm_single_analysis(
|
||
self,
|
||
intent: Dict[str, Any],
|
||
result: Dict[str, Any],
|
||
stock_code: str,
|
||
stock_name: Optional[str],
|
||
user_message: str
|
||
) -> Dict[str, Any]:
|
||
"""使用LLM对单一查询进行分析"""
|
||
data = result.get("data", result)
|
||
display_name = stock_name or stock_code
|
||
|
||
# 根据查询类型构建不同的prompt
|
||
if intent["type"] == "technical":
|
||
prompt = f"""你是一位专业的股票分析师。用户询问了{display_name}({stock_code})的技术指标。
|
||
|
||
用户问题:{user_message}
|
||
|
||
【技术指标数据】
|
||
{json.dumps(data, ensure_ascii=False, indent=2)}
|
||
|
||
请进行专业的技术分析:
|
||
|
||
## 技术指标解读
|
||
1. 均线系统分析:
|
||
- 短期均线(MA5、MA10)与长期均线(MA20、MA60)的位置关系
|
||
- 判断当前趋势(多头/空头/震荡)
|
||
|
||
2. MACD指标分析:
|
||
- DIF和DEA的位置关系
|
||
- MACD柱状图的变化趋势
|
||
- 判断动能强弱
|
||
|
||
3. RSI指标分析:
|
||
- 当前RSI值的位置(超买/超卖/中性)
|
||
- 短期可能的走势
|
||
|
||
4. KDJ指标分析(如有):
|
||
- K、D、J值的位置关系
|
||
- 金叉/死叉信号
|
||
|
||
## 综合判断
|
||
- 短期走势预判(1-2周)
|
||
- 关键支撑位和压力位
|
||
- 操作建议(买入/持有/观望/减仓)
|
||
|
||
## 风险提示
|
||
- 主要技术风险点
|
||
|
||
写作要求:
|
||
1. 语言简洁专业,直接给出分析结论
|
||
2. 基于数据进行分析,不要编造
|
||
3. 控制在300-400字
|
||
4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
|
||
"""
|
||
|
||
elif intent["type"] == "quote":
|
||
prompt = f"""你是一位专业的股票分析师。用户询问了{display_name}({stock_code})的实时行情。
|
||
|
||
用户问题:{user_message}
|
||
|
||
【实时行情数据】
|
||
{json.dumps(data, ensure_ascii=False, indent=2)}
|
||
|
||
请进行专业的行情分析:
|
||
|
||
## 行情解读
|
||
1. 当日表现:
|
||
- 涨跌幅分析
|
||
- 成交量分析
|
||
- 振幅分析
|
||
|
||
2. 价格位置:
|
||
- 当前价格相对开盘价、最高价、最低价的位置
|
||
- 判断多空力量对比
|
||
|
||
3. 短期判断:
|
||
- 当日走势特征
|
||
- 短期可能的走势
|
||
- 操作建议
|
||
|
||
写作要求:
|
||
1. 语言简洁专业,直接给出分析结论
|
||
2. 基于数据进行分析,不要编造
|
||
3. 控制在200-300字
|
||
4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
|
||
"""
|
||
|
||
elif intent["type"] == "fundamental":
|
||
prompt = f"""你是一位专业的股票分析师。用户询问了{display_name}({stock_code})的基本面信息。
|
||
|
||
用户问题:{user_message}
|
||
|
||
【基本面数据】
|
||
{json.dumps(data, ensure_ascii=False, indent=2)}
|
||
|
||
请进行专业的基本面分析:
|
||
|
||
## 公司概况
|
||
- 公司主营业务
|
||
- 所属行业和地域
|
||
- 上市时间和市场
|
||
|
||
## 行业分析
|
||
- 所属行业的发展前景
|
||
- 行业地位和竞争优势
|
||
|
||
## 投资价值
|
||
- 基本面评估
|
||
- 长期投资价值
|
||
- 关注要点
|
||
|
||
写作要求:
|
||
1. 语言简洁专业,直接给出分析结论
|
||
2. 基于数据进行分析,不要编造
|
||
3. 控制在200-300字
|
||
4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
|
||
"""
|
||
|
||
else:
|
||
# 其他类型,使用通用分析
|
||
prompt = f"""你是一位专业的股票分析师。用户询问了{display_name}({stock_code})的相关信息。
|
||
|
||
用户问题:{user_message}
|
||
|
||
【数据】
|
||
{json.dumps(data, ensure_ascii=False, indent=2)}
|
||
|
||
请基于提供的数据进行专业分析,给出有价值的见解和建议。
|
||
|
||
写作要求:
|
||
1. 语言简洁专业,直接给出分析结论
|
||
2. 基于数据进行分析,不要编造
|
||
3. 控制在200-300字
|
||
4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
|
||
"""
|
||
|
||
try:
|
||
analysis = llm_service.chat(
|
||
messages=[{"role": "user", "content": prompt}],
|
||
temperature=0.7,
|
||
max_tokens=1500
|
||
)
|
||
|
||
if analysis:
|
||
return {
|
||
"message": f"【{display_name}({stock_code}) - AI分析】\n\n{analysis}",
|
||
"metadata": {"type": intent["type"], "data": data}
|
||
}
|
||
else:
|
||
# LLM失败,使用原始格式化
|
||
return self._format_response(intent, result, stock_code, stock_name)
|
||
|
||
except Exception as e:
|
||
logger.error(f"LLM单一分析失败: {e}")
|
||
return self._format_response(intent, result, stock_code, stock_name)
|
||
|
||
def _rule_based_analysis(self, data: Dict[str, Any]) -> str:
|
||
"""基于规则的分析(LLM不可用时的备选方案)"""
|
||
parts = [f"【{data['stock_name']}({data['stock_code']}) - 综合分析】\n"]
|
||
|
||
# 行情信息
|
||
if data.get('quote'):
|
||
quote = data['quote']
|
||
parts.append("## 一、实时行情")
|
||
parts.append(f"最新价:{quote.get('close', 0):.2f}元")
|
||
parts.append(f"涨跌幅:{quote.get('pct_chg', 0):.2f}%")
|
||
parts.append(f"成交量:{quote.get('vol', 0):.0f}手")
|
||
parts.append("")
|
||
|
||
# 技术分析
|
||
if data.get('technical'):
|
||
tech = data['technical'].get('indicators', {})
|
||
parts.append("## 二、技术指标")
|
||
|
||
if 'ma' in tech:
|
||
ma = tech['ma']
|
||
parts.append(f"均线系统:MA5={ma.get('ma5')}, MA10={ma.get('ma10')}, MA20={ma.get('ma20')}")
|
||
|
||
if 'macd' in tech:
|
||
macd = tech['macd']
|
||
parts.append(f"MACD:DIF={macd.get('dif')}, DEA={macd.get('dea')}")
|
||
|
||
if 'rsi' in tech:
|
||
rsi = tech['rsi']
|
||
rsi6 = rsi.get('rsi6', 50)
|
||
if rsi6 > 70:
|
||
parts.append(f"RSI:{rsi6:.1f}(超买区域,注意回调风险)")
|
||
elif rsi6 < 30:
|
||
parts.append(f"RSI:{rsi6:.1f}(超卖区域,可能存在反弹机会)")
|
||
else:
|
||
parts.append(f"RSI:{rsi6:.1f}(中性区域)")
|
||
|
||
parts.append("")
|
||
|
||
# 基本面
|
||
if data.get('fundamental'):
|
||
fund = data['fundamental']
|
||
parts.append("## 三、基本信息")
|
||
parts.append(f"所属行业:{fund.get('industry', '未知')}")
|
||
parts.append(f"上市日期:{fund.get('list_date', '未知')}")
|
||
parts.append("")
|
||
|
||
# 简单建议
|
||
parts.append("## 四、参考建议")
|
||
parts.append("建议结合更多信息进行综合判断。")
|
||
parts.append("")
|
||
parts.append("⚠️ 以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。")
|
||
|
||
return "\n".join(parts)
|
||
|
||
async def _single_query(
|
||
self,
|
||
stock_code: str,
|
||
stock_name: Optional[str],
|
||
message: str
|
||
) -> Dict[str, Any]:
|
||
"""单一查询处理 - 使用LLM进行分析"""
|
||
# 识别意图
|
||
intent = self._recognize_intent(message, stock_code)
|
||
|
||
# 执行技能
|
||
result = await skill_manager.execute_skill(
|
||
intent["skill"],
|
||
**intent["params"]
|
||
)
|
||
|
||
# 格式化响应
|
||
if not result.get("success", True):
|
||
return {
|
||
"message": f"查询失败:{result.get('error', '未知错误')}",
|
||
"metadata": {"type": "error"}
|
||
}
|
||
|
||
# 所有查询都使用LLM进行分析(除了可视化)
|
||
if intent["type"] != "visualization" and self.use_llm:
|
||
return await self._llm_single_analysis(intent, result, stock_code, stock_name, message)
|
||
else:
|
||
return self._format_response(intent, result, stock_code, stock_name)
|
||
|
||
def _recognize_intent(self, message: str, stock_code: str) -> Dict[str, Any]:
|
||
"""识别查询意图"""
|
||
message_lower = message.lower()
|
||
|
||
# K线图
|
||
if any(kw in message_lower for kw in ["k线", "图表", "走势图", "kline"]):
|
||
return {
|
||
"type": "visualization",
|
||
"skill": "visualization",
|
||
"params": {"stock_code": stock_code}
|
||
}
|
||
|
||
# 技术分析
|
||
if any(kw in message_lower for kw in ["技术", "指标", "macd", "rsi", "均线"]):
|
||
return {
|
||
"type": "technical",
|
||
"skill": "technical_analysis",
|
||
"params": {"stock_code": stock_code, "indicators": ["ma", "macd", "rsi"]}
|
||
}
|
||
|
||
# 基本面
|
||
if any(kw in message_lower for kw in ["基本面", "公司", "行业", "信息"]):
|
||
return {
|
||
"type": "fundamental",
|
||
"skill": "fundamental",
|
||
"params": {"stock_code": stock_code}
|
||
}
|
||
|
||
# 默认:实时行情
|
||
return {
|
||
"type": "quote",
|
||
"skill": "market_data",
|
||
"params": {"stock_code": stock_code, "data_type": "quote"}
|
||
}
|
||
|
||
def _format_response(
|
||
self,
|
||
intent: Dict[str, Any],
|
||
result: Dict[str, Any],
|
||
stock_code: str,
|
||
stock_name: Optional[str]
|
||
) -> Dict[str, Any]:
|
||
"""格式化响应"""
|
||
data = result.get("data", result)
|
||
display_name = stock_name or stock_code
|
||
|
||
if intent["type"] == "quote":
|
||
message = f"""【{display_name}】实时行情
|
||
|
||
交易日期:{data.get('trade_date', '')}
|
||
最新价:{data.get('close', 0):.2f}元
|
||
涨跌幅:{data.get('pct_chg', 0):+.2f}%
|
||
涨跌额:{data.get('change', 0):+.2f}元
|
||
开盘价:{data.get('open', 0):.2f}元
|
||
最高价:{data.get('high', 0):.2f}元
|
||
最低价:{data.get('low', 0):.2f}元
|
||
成交量:{data.get('vol', 0):.0f}手
|
||
成交额:{data.get('amount', 0):.0f}千元"""
|
||
|
||
return {
|
||
"message": message,
|
||
"metadata": {"type": "quote", "data": data}
|
||
}
|
||
|
||
elif intent["type"] == "technical":
|
||
indicators = data.get("indicators", {})
|
||
parts = [f"【{display_name}】技术指标\n"]
|
||
|
||
if "ma" in indicators:
|
||
ma = indicators["ma"]
|
||
parts.append(f"均线:MA5={ma.get('ma5')}, MA10={ma.get('ma10')}, MA20={ma.get('ma20')}")
|
||
|
||
if "macd" in indicators:
|
||
macd = indicators["macd"]
|
||
parts.append(f"MACD:DIF={macd.get('dif')}, DEA={macd.get('dea')}, MACD={macd.get('macd')}")
|
||
|
||
if "rsi" in indicators:
|
||
rsi = indicators["rsi"]
|
||
parts.append(f"RSI:RSI6={rsi.get('rsi6')}, RSI12={rsi.get('rsi12')}")
|
||
|
||
return {
|
||
"message": "\n".join(parts),
|
||
"metadata": {"type": "technical", "data": data}
|
||
}
|
||
|
||
elif intent["type"] == "visualization":
|
||
return {
|
||
"message": f"已生成【{display_name}】的K线图",
|
||
"metadata": {"type": "chart", "data": data}
|
||
}
|
||
|
||
elif intent["type"] == "fundamental":
|
||
message = f"""【{display_name}】基本信息
|
||
|
||
股票代码:{data.get('ts_code', '')}
|
||
所属地域:{data.get('area', '')}
|
||
所属行业:{data.get('industry', '')}
|
||
上市市场:{data.get('market', '')}
|
||
上市日期:{data.get('list_date', '')}"""
|
||
|
||
return {
|
||
"message": message,
|
||
"metadata": {"type": "fundamental", "data": data}
|
||
}
|
||
|
||
return {
|
||
"message": "查询完成",
|
||
"metadata": {"type": "data", "data": data}
|
||
}
|
||
|
||
async def _analyze_question_intent(self, message: str) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
使用LLM分析问题意图
|
||
|
||
Args:
|
||
message: 用户消息
|
||
|
||
Returns:
|
||
意图分析结果: {
|
||
'type': 'stock_specific' | 'macro_finance' | 'knowledge',
|
||
'description': '问题描述',
|
||
'keywords': ['关键词列表'],
|
||
'stock_names': ['股票名称'] (如果是stock_specific类型)
|
||
}
|
||
"""
|
||
if not self.use_llm:
|
||
logger.warning("LLM未配置,无法分析意图")
|
||
return None
|
||
|
||
prompt = f"""分析用户的金融问题,判断问题类型和关键信息。
|
||
|
||
用户问题:{message}
|
||
|
||
请分析这个问题属于以下哪一类:
|
||
|
||
1. **stock_specific** - 针对特定股票的问题
|
||
例如:"贵州茅台怎么样"、"分析一下比亚迪"、"600519的技术指标"
|
||
|
||
2. **macro_finance** - 宏观金融问题(不针对特定股票)
|
||
例如:"现在A股市场怎么样"、"最近有什么投资机会"、"如何看待当前经济形势"
|
||
|
||
3. **knowledge** - 金融知识问答
|
||
例如:"什么是MACD"、"如何看K线图"、"价值投资是什么"
|
||
|
||
请以JSON格式返回分析结果:
|
||
{{
|
||
"type": "问题类型",
|
||
"description": "问题的简要描述",
|
||
"keywords": ["关键词1", "关键词2"],
|
||
"stock_names": ["股票名称"] (仅当type为stock_specific时)
|
||
}}
|
||
|
||
只返回JSON,不要有任何其他内容。"""
|
||
|
||
try:
|
||
result = llm_service.chat(
|
||
messages=[{"role": "user", "content": prompt}],
|
||
temperature=0.3,
|
||
max_tokens=300
|
||
)
|
||
|
||
if not result:
|
||
logger.warning("LLM返回空结果")
|
||
return None
|
||
|
||
# 清理结果,移除可能的markdown代码块标记
|
||
result = result.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:
|
||
logger.warning("LLM返回内容为空")
|
||
return None
|
||
|
||
# 解析JSON
|
||
intent = json.loads(result)
|
||
logger.info(f"意图分析结果: {intent}")
|
||
return intent
|
||
|
||
except json.JSONDecodeError as e:
|
||
logger.error(f"意图分析JSON解析失败: {e}, 原始响应: {result[:200] if result else 'None'}")
|
||
return None
|
||
except Exception as e:
|
||
logger.error(f"意图分析失败: {e}")
|
||
return None
|
||
|
||
async def _handle_stock_question(
|
||
self,
|
||
intent_analysis: Dict[str, Any],
|
||
message: str
|
||
) -> Dict[str, Any]:
|
||
"""处理针对特定股票的问题"""
|
||
stock_names = intent_analysis.get('stock_names', [])
|
||
|
||
if not stock_names:
|
||
return {
|
||
"message": "抱歉,我没有识别到您提到的股票。请提供更明确的股票代码或名称。",
|
||
"metadata": {"type": "error"}
|
||
}
|
||
|
||
# 提取第一个股票(暂时只处理单只股票)
|
||
stock_keyword = stock_names[0]
|
||
|
||
# 使用Tushare搜索股票
|
||
search_results = tushare_service.search_stock(stock_keyword)
|
||
|
||
if not search_results:
|
||
return {
|
||
"message": f"抱歉,未找到股票\"{stock_keyword}\"。请确认股票名称或代码是否正确。",
|
||
"metadata": {"type": "error"}
|
||
}
|
||
|
||
stock = search_results[0]
|
||
stock_code = stock['symbol']
|
||
stock_name = stock['name']
|
||
|
||
logger.info(f"处理股票问题: {stock_name}({stock_code})")
|
||
|
||
# 判断是否需要全面分析
|
||
is_comprehensive = self._is_comprehensive_analysis(message)
|
||
|
||
if is_comprehensive:
|
||
return await self._comprehensive_analysis(stock_code, stock_name, message)
|
||
else:
|
||
return await self._single_query(stock_code, stock_name, message)
|
||
|
||
async def _handle_macro_question(
|
||
self,
|
||
intent_analysis: Dict[str, Any],
|
||
message: str
|
||
) -> Dict[str, Any]:
|
||
"""处理宏观金融问题"""
|
||
keywords = intent_analysis.get('keywords', [])
|
||
description = intent_analysis.get('description', '')
|
||
|
||
logger.info(f"处理宏观问题: {description}")
|
||
|
||
# 使用Brave搜索获取最新信息
|
||
search_query = f"A股市场 {' '.join(keywords)} 最新分析"
|
||
|
||
try:
|
||
news_result = await skill_manager.execute_skill(
|
||
"brave_search",
|
||
query=search_query,
|
||
search_type="news",
|
||
count=5,
|
||
freshness="pw"
|
||
)
|
||
|
||
# 构建新闻摘要
|
||
news_summary = ""
|
||
if news_result and not news_result.get("error"):
|
||
results = news_result.get("results", [])
|
||
if results:
|
||
news_summary = "\n【最新市场动态】\n"
|
||
for idx, news_item in enumerate(results[:5], 1):
|
||
news_summary += f"{idx}. {news_item.get('title', '无标题')}\n"
|
||
news_summary += f" 来源: {news_item.get('source', '未知')}\n"
|
||
news_summary += f" 时间: {news_item.get('published', '未知')}\n\n"
|
||
|
||
# 使用LLM进行分析
|
||
prompt = f"""你是一位专业的金融分析师。用户询问了宏观金融问题。
|
||
|
||
用户问题:{message}
|
||
|
||
问题分析:{description}
|
||
关键词:{', '.join(keywords)}
|
||
{news_summary}
|
||
|
||
请基于当前市场情况和最新动态,给出专业的分析和建议:
|
||
|
||
## 市场现状分析
|
||
- 当前市场整体情况
|
||
- 主要影响因素
|
||
|
||
## 趋势判断
|
||
- 短期趋势
|
||
- 中长期展望
|
||
|
||
## 投资建议
|
||
- 投资策略建议
|
||
- 风险提示
|
||
|
||
写作要求:
|
||
1. 语言简洁专业,避免过度修饰
|
||
2. 分析要客观理性,基于事实
|
||
3. 控制在400-500字
|
||
4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
|
||
"""
|
||
|
||
analysis = llm_service.chat(
|
||
messages=[{"role": "user", "content": prompt}],
|
||
temperature=0.7,
|
||
max_tokens=1500
|
||
)
|
||
|
||
if analysis:
|
||
return {
|
||
"message": f"【宏观市场分析】\n\n{analysis}",
|
||
"metadata": {"type": "macro_analysis"}
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"宏观问题处理失败: {e}")
|
||
|
||
return {
|
||
"message": "抱歉,暂时无法获取相关信息。请稍后再试。",
|
||
"metadata": {"type": "error"}
|
||
}
|
||
|
||
async def _handle_knowledge_question(
|
||
self,
|
||
intent_analysis: Dict[str, Any],
|
||
message: str
|
||
) -> Dict[str, Any]:
|
||
"""处理金融知识问答"""
|
||
description = intent_analysis.get('description', '')
|
||
keywords = intent_analysis.get('keywords', [])
|
||
|
||
logger.info(f"处理知识问答: {description}")
|
||
|
||
# 直接使用LLM回答
|
||
prompt = f"""你是一位专业的金融教育专家。用户询问了金融知识问题。
|
||
|
||
用户问题:{message}
|
||
|
||
请用通俗易懂的语言解释这个概念或回答这个问题:
|
||
|
||
## 核心概念
|
||
- 清晰定义和解释
|
||
|
||
## 实际应用
|
||
- 如何在投资中应用
|
||
- 注意事项
|
||
|
||
## 举例说明
|
||
- 用简单的例子帮助理解
|
||
|
||
写作要求:
|
||
1. 语言通俗易懂,避免过多专业术语
|
||
2. 如果使用专业术语,要简单解释
|
||
3. 控制在300-400字
|
||
4. 重点是帮助用户理解,而不是炫耀知识
|
||
"""
|
||
|
||
try:
|
||
answer = llm_service.chat(
|
||
messages=[{"role": "user", "content": prompt}],
|
||
temperature=0.7,
|
||
max_tokens=1200
|
||
)
|
||
|
||
if answer:
|
||
return {
|
||
"message": f"【金融知识解答】\n\n{answer}",
|
||
"metadata": {"type": "knowledge"}
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"知识问答处理失败: {e}")
|
||
|
||
return {
|
||
"message": "抱歉,暂时无法回答您的问题。请稍后再试。",
|
||
"metadata": {"type": "error"}
|
||
}
|
||
|
||
|
||
# 创建全局实例
|
||
smart_agent = SmartStockAgent()
|