练习:基于A2A打造多Agent金融项目

本文参考自邢云阳《AI Agent开发实战》,为读书的一个练习。

本次练习基于python3.11,conda搭建虚拟环境。

一、AKShare试用

AKShare是一个免费的金融数据获取工具。首先pip安装依赖:

$ pip install akshare --upgrade

安装成功后,下面我们来试用一下。我们以宁德时代为例,抓取下历史日K数据:

python 复制代码
import akshare as ak

df = ak.stock_zh_a_hist(symbol="300750",
                        period="daily",
                        start_date="20250407",
                        end_date="20250411",
                        adjust="qfq")

df

股票分析中,有一门技术叫移动平均线,通常需要将日期逆序排序,以便从最早的时间点开始逐日滚动计算平均值:

python 复制代码
df.set_index('日期', inplace=True)
df.sort_index(ascending=False, inplace=True)
df

我们可以通过下面的方式显示数据各个字段的类型:

python 复制代码
df.dtypes

可以看到"日期"是object类型,为了进行排序,我们可以转换成datetime类型:

python 复制代码
import pandas as pd
df['日期'] = pd.to_datetime(df['日期'])
df.dtypes

最后,我们再试试获取创业板实时行情:

python 复制代码
stock_cy_a_spot_em_df = ak.stock_cy_a_spot_em()
stock_cy_a_spot_em_df

总共获取到了1430条数据。

二、用自然语言查询股票名称与代码

首先我们实现一个股票信息查询工具,如下所示:

python 复制代码
from langchain_core.tools import tool
import akshare as ak

@tool
def get_stock_info(code: str, name: str) -> str:
    """可以根据传入的股票代码或股票名称获取股票信息
    Args:
        code: 股票代码
        name: 股票名称
    """
    code_isempty = (code == "" or len(code) <= 2)
    name_isempty = (name == "" or len(name) <= 2)

    if code_isempty and name_isempty:
        return []
    
    df = ak.stock_zh_a_spot_em() # 获取创业板股票列表

    ret = None
    if code_isempty and not name_isempty:
        ret = df[df['名称'].str.contains(name)]
    elif not code_isempty and name_isempty:
        ret = df[df['代码'].str.contains(code)]
    else:
        ret = df[df['代码'].str.contains(code) & df['名称'].str.contains(name)]

    return ret.to_dict(orient='records')

get_stock_info.invoke({"code": "", "name": "申通快递"})

接下来,我们采用两种方式来构建股票信息查询助手。

方式1:使用langgraph图节点

(1)LLM绑定工具:

python 复制代码
from langchain_openai import ChatOpenAI
from getpass import getpass

def DeepSeek():
    return ChatOpenAI(
        model= "deepseek-chat",
        api_key= getpass("deepseek api key: "),
        base_url="https://api.deepseek.com",
    )

tools = [get_stock_info]
tools_by_name = {tool.name: tool for tool in tools}
llm = DeepSeek()
llm_with_tools = llm.bind_tools(tools)

(2)定义节点函数:

python 复制代码
from langgraph.graph import MessagesState, StateGraph, START, END
from langchain_core.messages import SystemMessage, HumanMessage, ToolMessage
from typing_extensions import Literal
from IPython.display import Image, display

# Nodes
def llm_call(state: MessagesState):
    """LLM decides whether to call a tool or not"""
    print("------------------")
    print(state["messages"])
    print("------------------")
    # 创建消息列表
    messages = [
        SystemMessage(
            content="""
            你是一个股票助手,具备以下技能:
            1. 可以使用 get_stock_info 工具查询股票信息,该工具需要两个参数:
               - code: 股票代码(如果不知道可以传空字符串)
               - name: 股票名称(如果不知道可以传空字符串)

            规则:
            1. 请给出精简的回答,不要做任何的解释和说明
            2. 如果没有匹配到工具,则只会回复"对不起,我无法回答这个问题"
            """
        )
    ] + state["messages"]
    
    # 调用 LLM
    response = llm_with_tools.invoke(messages)
    
    return {
        "messages": [response]
    }


def tool_node(state: dict):
    """Performs the tool call"""
    #print("3333333333333")
    result = []
    for tool_call in state["messages"][-1].tool_calls:
        tool = tools_by_name[tool_call["name"]]
        observation = tool.invoke(tool_call["args"])
        # 将观察结果转换为字符串格式
        if isinstance(observation, list):
            # 如果是列表,将其转换为字符串表示
            observation = str(observation)

        result.append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
    return {"messages": result}


# Conditional edge function to route to the tool node or end based upon whether the LLM made a tool call
def should_continue(state: MessagesState) -> Literal["Action", "END"]:
    """Decide if we should continue the loop or stop based upon whether the LLM made a tool call"""
    #print("2222222222222")
    messages = state["messages"]
    last_message = messages[-1]
    # If the LLM makes a tool call, then perform an action
    if last_message.tool_calls:
        return "Action"
    # Otherwise, we stop (reply to the user)
    return "END"

(3)构建流程图:

python 复制代码
# Build workflow
agent_builder = StateGraph(MessagesState)

# Add nodes
agent_builder.add_node("llm_call", llm_call)
agent_builder.add_node("tool_node", tool_node)

# Add edges to connect nodes
agent_builder.add_edge(START, "llm_call")
agent_builder.add_conditional_edges(
    "llm_call",
    should_continue,
    {
        # Name returned by should_continue : Name of next node to visit
        "Action": "tool_node",
        "END": END,
    },
)
agent_builder.add_edge("tool_node", "llm_call")

# Compile the agent
agent = agent_builder.compile()

# Show the agent
display(Image(agent.get_graph(xray=True).draw_mermaid_png()))

流程图结果如下:

最后我们可以测试一下:

python 复制代码
# Invoke
messages = [HumanMessage(content="300750 是哪只股票的代码?")]
messages = agent.invoke({"messages": messages})
for m in messages["messages"]:
    m.pretty_print()

HumanMessage(content='300750 是哪只股票的代码?', additional_kwargs={}, response_metadata={}, id='6cf053fe-1ea1-4dc9-b6d0-074f8e94c8ff'), AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 60, 'prompt_tokens': 444, 'total_tokens': 504, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}, 'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 444}, 'model_provider': 'openai', 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_eaab8d114b_prod0820_fp8_kvcache', 'id': '000e3cea-2b53-46b7-8ab0-8725b7dd85e3', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019bd5cc-96a8-7301-a550-d11dffc3778c-0', tool_calls=\[{'name': 'get_stock_info', 'args': {'code': '300750', 'name': ''}, 'id': 'call_00_hJ77G1RtNolC3MTWFx9zrgp1', 'type': 'tool_call'}\], invalid_tool_calls=\[\], usage_metadata={'input_tokens': 444, 'output_tokens': 60, 'total_tokens': 504, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}}), ToolMessage(content="\[{'序号': 4394, '代码': '300750', '名称': '宁德时代', '最新价': 348.9, '涨跌幅': -0.97, '涨跌额': -3.42, '成交量': 338643.0, '成交额': 11842421263.8, '振幅': 2.84, '最高': 355.34, '最低': 345.33, '今开': 351.47, '昨收': 352.32, '量比': 0.94, '换手率': 0.8, '市盈率-动态': 24.36, '市净率': 5.07, '总市值': 1592311036963.0, '流通市值': 1485119746352.0, '涨速': 0.16, '5分钟涨跌': 0.17, '60日涨跌幅': -7.32, '年初至今涨跌幅': -5.0}\]", id='0cf58eef-4c42-455e-ae2d-0cf362e40d31', tool_call_id='call_00_hJ77G1RtNolC3MTWFx9zrgp1')


================================[1m Human Message [0m=================================

300750 是哪只股票的代码?

==================================[1m Ai Message [0m==================================

Tool Calls:

get_stock_info (call_00_hJ77G1RtNolC3MTWFx9zrgp1)

Call ID: call_00_hJ77G1RtNolC3MTWFx9zrgp1

Args:

code: 300750

name:

=================================[1m Tool Message [0m=================================

{'序号': 4394, '代码': '300750', '名称': '宁德时代', '最新价': 348.9, '涨跌幅': -0.97, '涨跌额': -3.42, '成交量': 338643.0, '成交额': 11842421263.8, '振幅': 2.84, '最高': 355.34, '最低': 345.33, '今开': 351.47, '昨收': 352.32, '量比': 0.94, '换手率': 0.8, '市盈率-动态': 24.36, '市净率': 5.07, '总市值': 1592311036963.0, '流通市值': 1485119746352.0, '涨速': 0.16, '5分钟涨跌': 0.17, '60日涨跌幅': -7.32, '年初至今涨跌幅': -5.0}

==================================[1m Ai Message [0m==================================

宁德时代

方式2:Pre-built Agent

这种方式简单来说就是使用框架已经预构建的工作流:

python 复制代码
# 新的、推荐的导入方式
from langchain.agents import create_agent
from IPython.display import Image, display

pre_built_agent = create_agent(llm, tools)

# 保存代理工作流程图到文件
# graph_png = pre_built_agent.get_graph(xray=True).draw_mermaid_png()
# with open("agent_graph.png", "wb") as f:
#     f.write(graph_png)

# Show the agent
display(Image(pre_built_agent.get_graph(xray=True).draw_mermaid_png()))

测试调用与上面差不多,这里就不演示了。

三、抓取沪深A股全部股票的日K数据

准备抓取数据的代码:

python 复制代码
import asyncio
from typing import List
import akshare as ak
import pandas as pd

async def save_data(codes:List[str], start_date:str, end_date:str,prefix:str):
     all_data= pd.DataFrame()
     tasklist=[]
     for code in codes:
         task=asyncio.create_task(load_data(code,start_date,end_date))
         tasklist.append(task)
     ret=await asyncio.gather(*tasklist)
     for r in ret:
         all_data=pd.concat([all_data, r],axis=0)
     filename="{} {}_{}.csv".format(prefix,start_date,end_date)
     all_data.to_csv("./data/{}".format(filename))
     print("保存所有日线数据完成,文件名是:{}".format(filename))

async def load_data(symbol, start_date, end_date):
    # 由于 akshare 的 API 是同步的,我们需要在线程池中运行它
    loop = asyncio.get_event_loop()
    df = await loop.run_in_executor(None, lambda: ak.stock_zh_a_hist(
        symbol=symbol, 
        period="daily", 
        start_date=start_date, 
        end_date=end_date, 
        adjust="qfq"
    ))

    if df.empty:
        return pd.DataFrame()
    df['日期'] = pd.to_datetime(df['日期'])
    df.set_index('日期', inplace=True)
    df.sort_index(ascending=False, inplace=True)

    return df

下面我们可以尝试抓取过去两年的数据:

(1)获取股票代码:

python 复制代码
def get_all_codes():
    df=ak.stock_zh_a_spot_em()
    codes=df['代码']
    bool_list=df['代码'].str.startswith(('60','30','00','68'))
    return codes[bool_list].to_list()

(2)根据股票代码并发抓取日K数据:

python 复制代码
def save_all_data():
    codes=get_all_codes()
    print("共有{}个股票需要抓取".format(len(codes)))
    n=100
    for i in range(0, len(codes), n):
        subset = codes[i:i + n]
        if len(subset) > 0:
            asyncio.run(save_data(subset,'20230422','20250422',
                                  prefix=f"{i}_"))
            print("抓取了{}".format(i))

(3)最后将所有抓到的数据合并成一个文件:

python 复制代码
import os
import re
import pandas as pd


def load_df(file:str)->pd.DataFrame:
     df=pd.read_csv("./data/{}".format(file))
     if df.empty:
         raise Exception("文件不存在")
     df['日期'] = pd.to_datetime(df['日期'])
     df['股票代码'] = df['股票代码'].astype(str).str.zfill(6)  # Convert to string and pad with zeros to ensure 6 digits
     return df

def concat_csv(file_name:str):
    folder_path = './data'
    # 列出文件夹中的所有文件和目录
    files = os.listdir(folder_path)
    # 定义一个正则表达式,匹配以数字开头的文件名
    pattern = re.compile(r'^\d+_.+\.csv$')
    # 遍历文件,筛选出符合条件的文件名
    filtered_files = [file for file in files if pattern.match(file)]
    ret=pd.DataFrame()
    # 打印结果
    for file in filtered_files:
        df=load_df(file)
        ret=pd.concat([ret,df])
    ret.to_csv("./{}".format(file_name))
    print("合并完成,文件名是{}".format(file_name))

四、计划模式:让Agent有计划地分析股票数据

在开始之前,我们首先需要获取财报数据,如下所示:

python 复制代码
# 财报获取方法
import akshare as ak
import pandas as pd

df = ak.stock_yjbb_em(date="20241231")
df.to_csv("financial_report.csv")

(1)实现指标计算工具与财报工具

python 复制代码
# 指标计算工具

import pandas as pd
import matplotlib.pyplot as plt
import os
import matplotlib as mpl
import numpy as np
from langchain_core.tools import tool

@tool
def analyze_stocks(stock_codes):
    """
    根据股票代码列表获取股票的起始价格,结束价格,区间涨跌幅,最大回撤,年化波动率
    
    Parameters:
    -----------
    stock_codes : list
        股票代码列表
    """

    start_date='20240422'
    end_date='20250422'

    # 设置中文字体
    plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'Arial Unicode MS']
    plt.rcParams['axes.unicode_minus'] = False
    mpl.rcParams['font.family'] = 'sans-serif'
    
    # 读取数据
    data_path = os.path.join('./', 'all_data.csv')
    df = pd.read_csv(data_path)
    
    # 转换日期格式
    df['日期'] = pd.to_datetime(df['日期'])
    
    # 创建结果列表
    all_results = []
    
    # 创建图表
    plt.figure(figsize=(15, 8))
    
    # 转换日期参数
    start_date = pd.to_datetime(start_date)
    end_date = pd.to_datetime(end_date)
    
    # 为每个股票代码进行分析
    for stock_code in stock_codes:
        try:
            # 筛选股票数据
            print("------stock_code-------")
            print(stock_code)
            print("------------------")
            stock_data = df[df['股票代码'].astype(str).str.zfill(6) == stock_code].copy()
            
            if stock_data.empty:
                print(f"警告: 未找到股票: {stock_code}")
                continue
            
            # 筛选日期范围
            stock_data = stock_data[(stock_data['日期'] >= start_date) & (stock_data['日期'] <= end_date)]
            
            if len(stock_data) < 2:  # 确保至少有两条数据
                print(f"警告: 在指定日期范围内数据不足: {stock_code}")
                continue
            
            # 按日期排序
            stock_data = stock_data.sort_values('日期')
            
            # 计算日收益率
            stock_data['日收益率'] = stock_data['收盘'].pct_change()
            
            # 计算年化波动率 (假设一年252个交易日)
            volatility = stock_data['日收益率'].std() * np.sqrt(252) * 100
            
            # 计算关键指标
            start_price = stock_data.iloc[0]['收盘']
            end_price = stock_data.iloc[-1]['收盘']
            total_return = (end_price - start_price) / start_price * 100
            
            # 计算最大回撤
            stock_data['max_price'] = stock_data['收盘'].cummax()
            stock_data['min_price'] = stock_data['收盘'].cummin()
            stock_data['drawdown'] = (stock_data['max_price'] - stock_data['min_price']) / stock_data['max_price'] * 100
            max_drawdown = stock_data['drawdown'].max()
            
            # 添加到结果列表
            all_results.append({
                '股票代码': stock_code,
                '起始价格': start_price,
                '结束价格': end_price,
                '区间涨跌幅(%)': total_return,
                '最大回撤(%)': max_drawdown,
                '年化波动率(%)': volatility
            })
            
            # 绘制股价走势图
            plt.plot(stock_data['日期'], stock_data['收盘'], label=f'{stock_code}')
            
            # 添加关键价格标注
            plt.annotate(f'{stock_code} 起始价: {start_price:.2f}', 
                        xy=(stock_data['日期'].iloc[0], start_price),
                        xytext=(10, 10), textcoords='offset points')
            plt.annotate(f'{stock_code} 结束价: {end_price:.2f}',
                        xy=(stock_data['日期'].iloc[-1], end_price),
                        xytext=(10, -10), textcoords='offset points')
            
            # 标注最大回撤点
            max_drawdown_idx = stock_data['drawdown'].idxmax()
            if max_drawdown_idx is not None and max_drawdown_idx in stock_data.index:
                plt.annotate(f'{stock_code} 最大回撤: {max_drawdown:.2f}%',
                            xy=(stock_data.loc[max_drawdown_idx, '日期'], stock_data.loc[max_drawdown_idx, '收盘']),
                            xytext=(10, -10), textcoords='offset points')
            
        except Exception as e:
            print(f"处理股票 {stock_code} 时出错: {str(e)}")
            continue
    
    if not all_results:
        raise ValueError("没有找到任何有效的股票数据")
    
    # 创建结果DataFrame
    results = pd.DataFrame(all_results)
    
    # 保存结果到CSV
    output_dir = os.path.join('tmp', 'output')
    os.makedirs(output_dir, exist_ok=True)
    results.to_csv(os.path.join(output_dir, 'stocks_analysis.csv'), index=False, encoding='utf-8-sig')
    
    # 完善图表
    plt.title(f'股价走势图 ({start_date.strftime("%Y-%m-%d")} 至 {end_date.strftime("%Y-%m-%d")})')
    plt.xlabel('日期')
    plt.ylabel('价格')
    plt.grid(True)
    plt.legend()
    
    # 调整x轴日期显示
    plt.gcf().autofmt_xdate()
    
    # 保存图表
    plt.savefig(os.path.join(output_dir, 'stocks_price_chart.png'), dpi=300, bbox_inches='tight')
    plt.close()
    
    return results

可以使用下面的代码简单测试下:

python 复制代码
# 示例使用
try:
    stock_codes = ['600600','300054','600698','600573']  # 可以替换为您想要分析的股票代码列表
    results = analyze_stocks.invoke({"stock_codes": stock_codes})
    print("\n分析结果:")
    print(results)
except Exception as e:
    print(f"错误: {str(e)}")

下面实现财报工具:

python 复制代码
# 财报工具

from langchain_core.tools import tool
import pandas as pd
import os

@tool
def get_financial_report(stock_codes):
    """
    根据股票代码列表获取财报数据
    
    Parameters:
    -----------
    stock_codes : list
        股票代码列表
    
    Returns:
    --------
    dict
        包含每个股票代码对应的财报数据的字典
    """
    # 确保数据目录存在
    data_dir = './'
    os.makedirs(data_dir, exist_ok=True)
    
    try:
        # 读取CSV文件
        df = pd.read_csv(os.path.join(data_dir, 'financial_report.csv'))
        print("从本地文件读取数据成功")
        
        # 确保股票代码列是字符串类型
        df['股票代码'] = df['股票代码'].astype(str).str.zfill(6)
        
        # 创建结果字典
        result = {}
        
        # 为每个股票代码获取数据
        for code in stock_codes:
            # 确保股票代码格式一致(6位数字)
            code = str(code).zfill(6)
            # 筛选该股票的数据
            stock_data = df[df['股票代码'] == code]
           
            if not stock_data.empty:
                # 将数据转换为字典格式,包含列名
                result[code] = {
                    'data': stock_data.to_dict('records')
                }
            else:
                result[code] = {
                    'data': []
                }
        
        return result
    
    except Exception as e:
        print(f"读取数据时出错: {str(e)}")
        return None

测试使用:

python 复制代码
# 示例使用

# 测试用的股票代码列表
test_codes = ['600600','002461','000729','600573']
result = get_financial_report.invoke({"stock_codes": test_codes})

if result:
    # 打印结果
    for code, data in result.items():
        print(f"\n股票代码: {code}")
        print("数据内容:")
        for row in data['data']:
            print(row)
else:
    print("获取数据失败")

(2)计划模式的设计与代码实现

计划模式简单来说将解决问题分成了两个步骤:(1)指定计划。通过精心设计的提示词,让模型针对用户的提问制定详细的计划。(2)执行计划。执行计划过程中可以调用工具也可以不调用,直到执行完所有的计划才算做工作完成。要确定是否执行到了计划的最后一步,需要在提示此种加入特定的指示,如Final Answer,以便LLM能够在计划完成时明确告知,让我们能够基于回复做出判断。

计划提示词设计:

python 复制代码
plan_prompt = """
你是一个金融分析师,擅长使用工具对股票,上市公司财报等进行分析。请为用户提出的问题创建分析方案步骤:

可调用工具列表:
get_financial_report:
    根据股票代码列表获取财报数据
    
    Parameters:
    -----------
    stock_codes : list
        股票代码列表
    
    Returns:
    --------
    dict
        包含每个股票代码对应的财报数据的字典

analyze_stocks:
   根据股票代码列表获取股票的起始价格,结束价格,区间涨跌幅,最大回撤,年化波动率
    
    Parameters:
    -----------
    stock_codes : list
        股票代码列表

    Returns:
    --------
    DataFrame
        包含每个股票代码对应的起始价格,结束价格,区间涨跌幅,最大回撤,年化波动率

要求:
1.用中文列出清晰步骤
2.每个步骤标记序号
3.明确说明需要分析和执行的内容
4.只需输出计划内容,不要做任何额外的解释和说明
5.设计的方案步骤要紧紧贴合我的工具所能返回的内容,不要超出工具返回的内容
"""

计划节点实现如下:

python 复制代码
def plan_node(state):
    # 创建消息列表
    prompt = plan_prompt

    # 调用 LLM
    response = deepseek_r1.invoke([SystemMessage(content=prompt),state["messages"][0]])
    
    state["plan"] = response.content
    print(state["plan"])
    return state

LLM经过计划提示词引导,输出计划后,将通过State传递给后面的步骤中:

python 复制代码
from langgraph.graph import MessagesState

class State(MessagesState):
    plan: str

下面进行执行阶段的提示词设计与代码实现:

python 复制代码
def llm_call(state):
    """LLM decides whether to call a tool or not"""
    messages = [
        SystemMessage(
            content=f"""
你是一个思路清晰,有条理的金融分析师,必须严格按照以下金融分析计划执行:
    
当前金融分析计划:
{state["plan"]}

如果你认为计划已经执行到最后一步了,请在内容的末尾加上\nFinal Answer字样

示例:
分析报告xxxxxxxx
Final Answer
            """
        )
    ] + state["messages"]
    
    print("------messages[-1]-------")
    print(state["messages"][-1])
    print("------------------")

    # 调用 LLM
    response = llm_with_tools.invoke(messages)

    # 将响应添加到消息列表中
    state["messages"].append(response)

    return state

添加工具调用节点:

python 复制代码
def tool_node(state):
    """Performs the tool call"""
    result = []
    for tool_call in state["messages"][-1].tool_calls:
        tool = tools_by_name[tool_call["name"]]
        observation = tool.invoke(tool_call["args"])
        # 将观察结果转换为字符串格式
        if isinstance(observation, list):
            # 如果是列表,将其转换为字符串表示
            observation = str(observation)
        #result.append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
        state["messages"].append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
    return state

最后,添加条件分支节点的循环退出逻辑:

python 复制代码
# Conditional edge function to route to the tool node or end based upon whether the LLM made a tool call
def should_continue(state) -> Literal["environment", "END"]:
    """Decide if we should continue the loop or stop based upon whether the LLM made a tool call"""
    messages = state["messages"]
    last_message = messages[-1]
    # If the LLM makes a tool call, then perform an action
    if "Final Answer" in last_message.content:
        return "END"
    # Otherwise, we stop (reply to the user)
    return "environment"

下面我们将节点组装成工作流,并测试一下它的执行情况:

python 复制代码
# Build workflow
agent_builder = StateGraph(State)

# Add nodes
agent_builder.add_node("plan_node", plan_node)
agent_builder.add_node("llm_call", llm_call)
agent_builder.add_node("environment", tool_node)

# Add edges to connect nodes
agent_builder.add_edge(START, "plan_node")
agent_builder.add_edge("plan_node", "llm_call")
agent_builder.add_conditional_edges(
    "llm_call",
    should_continue,
    {
        # Name returned by should_continue : Name of next node to visit
        "environment": "environment",
        "END": END,
    },
)
agent_builder.add_edge("environment", "llm_call")

# Compile the agent
agent = agent_builder.compile()


# Show the agent
display(Image(agent.get_graph(xray=True).draw_mermaid_png()))

下面是测试代码:

python 复制代码
# Invoke
messages = [HumanMessage(content="对比一下 '600600', '002461', '000729', '600573' 这四只股票的股价表现和财务情况,哪家更值得投资")]
#question = "对比一下 600600, 002461, 000729, 600573的股价表现和财务情况,哪家更值得投资"
ret = agent.invoke({"plan": "", "messages": messages})

print(ret["messages"][-1].content)

输出的计划:

  1. 使用 analyze_stocks 工具获取股票代码 '600600', '002461', '000729', '600573' 的股价表现数据,包括起始价格、结束价格、区间涨跌幅、最大回撤、年化波动率。

  2. 使用 get_financial_report 工具获取股票代码 '600600', '002461', '000729', '600573' 的财报数据。

  3. 分析股价表现数据:对比各股票的区间涨跌幅、最大回撤和年化波动率,评估收益潜力和风险水平。

  4. 分析财报数据:从财报中提取并对比关键财务指标,如净利润、营业收入、负债比率等,评估财务健康状况。

  5. 综合股价表现和财务情况的分析结果,对四只股票进行整体评估,判断哪家更值得投资。

分析结果:

现在我来分析股价表现数据和财报数据:

股价表现数据分析

从股价表现数据来看:

  1. **区间涨跌幅**:
  • 000729(燕京啤酒):28.20%(表现最佳)

  • 002461(珠江啤酒):24.16%

  • 600573(惠泉啤酒):20.24%

  • 600600(青岛啤酒):1.18%(表现最差)

  1. **最大回撤**(风险指标):
  • 000729(燕京啤酒):39.09%(风险最高)

  • 600600(青岛啤酒):37.58%

  • 002461(珠江啤酒):32.23%

  • 600573(惠泉啤酒):28.11%(风险最低)

  1. **年化波动率**(波动性指标):
  • 600600(青岛啤酒):35.65%(波动最大)

  • 000729(燕京啤酒):30.24%

  • 600573(惠泉啤酒):28.61%

  • 002461(珠江啤酒):28.04%(波动最小)

财报数据分析

从财务数据来看:

  1. **盈利能力**:
  • **每股收益**:600600(3.191元)显著高于其他三只股票,其他三只均在0.26-0.37元之间

  • **净利润同比增长**:000729(63.74%)表现最佳,其次是600573(33.58%)、002461(29.95%),600600(1.81%)增长最慢

  • **净资产收益率**:600600(15.38%)远高于其他三只(5.09%-7.95%)

  1. **营收表现**:
  • **营业总收入**:600600(321.38亿元)规模最大,其次是000729(146.67亿元)、002461(57.31亿元)、600573(6.47亿元)

  • **营收同比增长**:002461(6.56%)表现最佳,600573(5.44%)、000729(3.20%)次之,600600(-5.30%)负增长

  1. **财务质量**:
  • **销售毛利率**:002461(46.30%)最高,其次是000729(40.72%)、600600(40.23%)、600573(31.18%)

  • **每股净资产**:600600(21.30元)最高,其他三只均在4.75-5.19元之间

综合评估

基于以上分析,我对四只股票进行综合评估:

1. 青岛啤酒(600600)

**优势**:

  • 盈利能力最强(每股收益3.191元,净资产收益率15.38%)

  • 规模最大,市场地位稳固

  • 财务质量较好

**劣势**:

  • 股价表现最差(区间涨跌幅仅1.18%)

  • 营收负增长(-5.30%)

  • 净利润增长缓慢(1.81%)

  • 波动性最大(年化波动率35.65%)

2. 珠江啤酒(002461)

**优势**:

  • 毛利率最高(46.30%)

  • 营收增长良好(6.56%)

  • 波动性最小(28.04%)

  • 股价表现较好(24.16%)

**劣势**:

  • 盈利能力中等

  • 规模相对较小

3. 燕京啤酒(000729)

**优势**:

  • 股价表现最佳(28.20%)

  • 净利润增长最快(63.74%)

  • 营收规模第二大

**劣势**:

  • 最大回撤最高(39.09%),风险较大

  • 净资产收益率相对较低(7.42%)

4. 惠泉啤酒(600573)

**优势**:

  • 最大回撤最低(28.11%),风险控制较好

  • 净利润增长较快(33.58%)

**劣势**:

  • 规模最小

  • 毛利率最低(31.18%)

  • 盈利能力最弱(净资产收益率5.09%)

投资建议

**综合考虑收益潜力、风险控制和财务健康状况,我推荐珠江啤酒(002461)作为最值得投资的股票**,理由如下:

  1. **平衡的风险收益比**:珠江啤酒在股价表现(24.16%涨幅)和风险控制(最大回撤32.23%,年化波动率28.04%)之间取得了较好的平衡。

  2. **良好的财务指标**:拥有最高的毛利率(46.30%),显示成本控制能力强;营收增长良好(6.56%),净利润增长稳健(29.95%)。

  3. **相对稳健**:相比燕京啤酒的高风险(最大回撤39.09%)和青岛啤酒的缓慢增长,珠江啤酒在增长和风险之间找到了更好的平衡点。

  4. **行业地位**:作为区域性啤酒品牌,在特定市场有竞争优势,且财务指标健康。

**投资优先级**:

  1. 珠江啤酒(002461) - 最佳平衡选择

  2. 燕京啤酒(000729) - 高增长潜力但风险较高

  3. 惠泉啤酒(600573) - 风险较低但增长潜力有限

  4. 青岛啤酒(600600) - 虽然财务基础好但增长乏力

Final Answer

五、量化分析Agent实战

(1)构建成交量计算工具

python 复制代码
import pandas as pd
from langchain_core.tools import tool

def calc_vol_ratio_around_date(df, target_date, days_before=3, days_after=3):
    """
    计算指定日期前后的成交量比值
    :param df: DataFrame,包含股票数据
    :param target_date: str,目标日期,格式:'YYYY-MM-DD'
    :param days_before: int,目标日期前的天数
    :param days_after: int,目标日期后的天数
    :return: float,成交量比值
    """
    # 将目标日期转换为datetime
    target_date = pd.to_datetime(target_date)
    
    # 获取目标日期在数据中的位置
    date_mask = df['日期'] == target_date
    if not date_mask.any():
        print(f"未找到日期 {target_date}")
        return None
    
    # 获取目标日期的索引
    target_idx = df[date_mask].index[0]
    
    # 获取前后的数据
    before_data = df.iloc[target_idx-days_before:target_idx]['成交量']
    after_data = df.iloc[target_idx:target_idx+days_after]['成交量']

    # 计算比值
    if len(before_data) == days_before and len(after_data) == days_after:
        return after_data.mean() / before_data.mean()
    else:
        print("数据不足,无法计算比值")
        return None

def load_df(file_path):
    # 读取数据
    df = pd.read_csv(file_path)
    # 将日期列转换为datetime类型
    df['日期'] = pd.to_datetime(df['日期'])
    return df

@tool
def vol_info(target_date:str):
    """
    计算指定日期后3天(含指定日期)与前3天的成交量比值
    param target_date: str,指定日期,格式:'YYYY-MM-DD'
    return: float,成交量比值
    """
    df = load_df('./600600.csv')
    ratio = calc_vol_ratio_around_date(df, target_date)

    return ratio

简单来说,这个方法计算选定日期前后平均交易量的比值。下面我们可以测试下这个方法:

python 复制代码
# 可以在这里修改目标日期
ratio =vol_info.invoke({"target_date": "2024-09-26"})

print(f"日期 2024-09-26 后3天与前3天的成交量比值: {ratio:.2f}")

日期 2024-09-26 后3天与前3天的成交量比值: 2.58

(2)构建股价信息获取工具

python 复制代码
from langchain_core.tools import tool
import pandas as pd

@tool
def stock_price(target_date:str):
    """
    获取股票指定日期前三天与后三天(包含指定日期)的收盘价
    param target_date: str,指定日期
    return: float,最新价格
    """
    df = load_df('600600.csv')
    
    # 将目标日期转换为datetime
    target_date = pd.to_datetime(target_date)
    
    # 获取目标日期在数据中的位置
    date_mask = df['日期'] == target_date
    if not date_mask.any():
        return f"未找到日期 {target_date}"
    
    # 获取目标日期的索引
    target_idx = df[date_mask].index[0]
    
    # 获取前后的数据并合并
    combined_data = pd.concat([
        df.iloc[target_idx-3:target_idx][['日期', '收盘']],
        df.iloc[target_idx:target_idx+3][['日期', '收盘']]
    ])
    
    # 格式化日期为字符串
    combined_data['日期'] = combined_data['日期'].dt.strftime('%Y-%m-%d')
    
    # 返回结果
    return combined_data.to_string(index=False)

该方法的功能如注释中所示。下面我们同样进行测试:

python 复制代码
result = stock_price.invoke({'target_date': '2024-09-26'})
print(result)

日期 收盘

2024-09-23 56.91

2024-09-24 59.46

2024-09-25 59.57

2024-09-26 65.53

2024-09-27 71.23

2024-09-30 78.35

(3)实现Agent

我们采用react模式实现Agent如下:

python 复制代码
from getpass import getpass

from langchain_openai import ChatOpenAI
from IPython.display import Image, display

def DeepSeek():
    return ChatOpenAI(
        model= "deepseek-chat",
        api_key= getpass("deepseek"),
        base_url="https://api.deepseek.com",
    )

from langchain.agents import create_agent
from langchain_core.messages import SystemMessage, HumanMessage

llm = DeepSeek()

pre_built_agent = create_agent(llm, tools=[stock_price, vol_info])

display(Image(pre_built_agent.get_graph(xray=True).draw_mermaid_png()))


# Invoke
prompt = """
你是一位金融分析师,擅长使用工具对股票进行量能分析。
工具1:stock_price
    获取股票指定日期前三天与后三天(包含指定日期)的收盘价
    
工具2:vol_info
    计算指定日期后3天(含指定日期)与前3天的成交量比值

要求:
需要分析出股票属于以下量价关系(量增价涨,量缩价涨,量增价跌,量缩价跌)中的哪一种,并给出分析结论
"""
messages = [SystemMessage(content=prompt), HumanMessage(content="600600 这只股票在 2024-09-26 左右的表现如何?")]
messages = pre_built_agent.invoke({"messages": messages})
for m in messages["messages"]:
    m.pretty_print()

下面是测试的结果:

================================ System Message ================================

你是一位金融分析师,擅长使用工具对股票进行量能分析。

工具1:stock_price

获取股票指定日期前三天与后三天(包含指定日期)的收盘价

工具2:vol_info

计算指定日期后3天(含指定日期)与前3天的成交量比值

要求:

需要分析出股票属于以下量价关系(量增价涨,量缩价涨,量增价跌,量缩价跌)中的哪一种,并给出分析结论

================================ Human Message =================================

600600 这只股票在 2024-09-26 左右的表现如何?

================================== Ai Message ==================================

我来帮您分析600600这只股票在2024-09-26左右的表现。首先让我获取相关的价格和成交量数据。

Tool Calls:

stock_price (call_00_ZzGy2bJ7wYQo2t48OnG3x3vR)

Call ID: call_00_ZzGy2bJ7wYQo2t48OnG3x3vR

Args:

target_date: 2024-09-26

================================= Tool Message =================================

Name: stock_price

日期 收盘

2024-09-23 56.91

2024-09-24 59.46

2024-09-25 59.57

2024-09-26 65.53

2024-09-27 71.23

2024-09-30 78.35

================================== Ai Message ==================================

现在让我获取成交量比值信息:

Tool Calls:

vol_info (call_00_xAnVDWoVwU4zjajzfHm5wFqk)

Call ID: call_00_xAnVDWoVwU4zjajzfHm5wFqk

Args:

target_date: 2024-09-26

================================= Tool Message =================================

Name: vol_info

2.5811135707934896

================================== Ai Message ==================================

根据获取的数据,我来为您分析600600这只股票在2024-09-26左右的表现:

数据分析:

价格走势分析(2024-09-23至2024-09-30):

  • 2024-09-23:56.91元

  • 2024-09-24:59.46元(上涨4.48%)

  • 2024-09-25:59.57元(微涨0.18%)

  • **2024-09-26:65.53元(大幅上涨10.01%)**

  • 2024-09-27:71.23元(上涨8.70%)

  • 2024-09-30:78.35元(上涨10.00%)

成交量分析:

  • 成交量比值:2.58倍

  • 这意味着2024-09-26及后3天的平均成交量是前3天平均成交量的2.58倍

量价关系判断:

**量增价涨** - 这是典型的量价齐升形态

分析结论:

  1. **强势上涨趋势**:从9月23日到9月30日,股价从56.91元上涨至78.35元,累计涨幅达到37.7%,表现非常强劲。

  2. **量能配合良好**:成交量放大2.58倍,说明上涨得到了充分的资金支持,不是单纯的投机炒作。

  3. **关键突破日**:9月26日是重要的突破日,当日涨幅达到10.01%(可能触及涨停),且成交量显著放大,显示市场对该股票有强烈的买入意愿。

  4. **技术面强势**:连续多日上涨且成交量持续放大,形成典型的量价齐升格局,通常预示着上涨趋势的可持续性较强。

  5. **市场情绪积极**:如此大幅度的量价齐升,通常表明市场对该股票的基本面或消息面有积极的预期。

**投资建议**:从技术分析角度看,这种量增价涨的形态是较为健康的上涨信号。但投资者也需要注意,短期内涨幅较大,可能存在技术性调整的需求。建议关注后续成交量是否能持续配合,以及是否有基本面支撑这一上涨趋势。

六、实现基于A2A的多Agent金融助手

前面我们分别实现了查询股票代码与名称的Agent、设定计划分析股票数据的Agent以及量化分析Agent,下面我们使用A2A协议将他们整合起来。

首先安装a2a依赖:

conda install a2a-sdk[http-server]

首先我们搭和用户交互的并负责与 Server 之间通信的 Client:

python 复制代码
# client/remote_agent_connection.py

from typing import Callable
import httpx
from a2a.client import A2AClient
from a2a.types import (
    AgentCard,
    Task,
    Message,
    MessageSendParams,
    TaskStatusUpdateEvent,
    TaskArtifactUpdateEvent,
    SendMessageRequest,
    SendStreamingMessageRequest,
    JSONRPCErrorResponse,
)
from uuid import uuid4

TaskCallbackArg = Task | TaskStatusUpdateEvent | TaskArtifactUpdateEvent
TaskUpdateCallback = Callable[[TaskCallbackArg, AgentCard], Task]


class RemoteAgentConnections:
    """A class to hold the connections to the remote agents."""

    def __init__(self, client: httpx.AsyncClient, agent_card: AgentCard):
        self.agent_client = A2AClient(client, agent_card)
        self.card = agent_card
        self.pending_tasks = set()

    def get_agent(self) -> AgentCard:
        return self.card

    async def send_message(
        self,
        request: MessageSendParams,
        task_callback: TaskUpdateCallback | None,
    ) -> Task | Message | None:
        if self.card.capabilities.streaming:
            task = None
            async for response in self.agent_client.send_message_streaming(
                SendStreamingMessageRequest(id=str(uuid4()), params=request)
            ):
                if not response.root.result:
                    return response.root.error
                # In the case a message is returned, that is the end of the interaction.
                event = response.root.result
                if isinstance(event, Message):
                    return event

                # Otherwise we are in the Task + TaskUpdate cycle.
                if task_callback and event:
                    task = task_callback(event, self.card)
                if hasattr(event, 'final') and event.final:
                    break
            return task
        else:  # Non-streaming
            response = await self.agent_client.send_message(
                SendMessageRequest(id=str(uuid4()), params=request)
            )
            if isinstance(response.root, JSONRPCErrorResponse):
                return response.root.error
            if isinstance(response.root.result, Message):
                return response.root.result

            if task_callback:
                task_callback(response.root.result, self.card)
            return response.root.result
python 复制代码
# client/client.py

import asyncio
from contextlib import AsyncExitStack
from openai import AsyncOpenAI
from typing import Any
import uuid
import json
import os
from getpass import getpass
import re
from a2a.client import A2ACardResolver
from a2a.types import (
    AgentCard,
    DataPart,
    Message,
    MessageSendConfiguration,
    MessageSendParams,
    Part,
    Task,
    TaskState,
    TextPart,
)
from remote_agent_connection import RemoteAgentConnections, TaskUpdateCallback
import httpx
from typing import List

class HostAgent:
    def __init__(
        self,
        remote_agent_addresses: list[str],
        http_client: httpx.AsyncClient,
        task_callback: TaskUpdateCallback | None = None,
    ):
        self.task_callback = task_callback
        self.httpx_client = http_client
        self.remote_agent_connections: dict[str, RemoteAgentConnections] = {}
        self.cards: dict[str, AgentCard] = {}
        self.agents = ''
        self.remote_agent_addresses = remote_agent_addresses
        self.client = AsyncOpenAI(
            api_key=getpass("AliDeep API KEY: "),  
            base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
        )

    async def connect_to_server(self):
        """Initialize the agent by fetching cards from remote agents"""
        for address in self.remote_agent_addresses:
            card_resolver = A2ACardResolver(httpx_client=self.httpx_client, base_url=address)
            card = await card_resolver.get_agent_card()
            remote_connection = RemoteAgentConnections(self.httpx_client, card)
            self.remote_agent_connections[card.name] = remote_connection
            self.cards[card.name] = card
        #列出可用的Agent
        agent_info = []
        for ra in self.list_remote_agents():
            agent_info.append(json.dumps(ra))
        self.agents = '\n'.join(agent_info)

    def list_remote_agents(self) -> List[dict]:
        """列出所有可用的远程Agent
        Returns:
            List[dict]: 一个包含Agent信息的字典列表
        """
        if not self.remote_agent_connections:
            return []

        remote_agent_info = []
        for card in self.cards.values():
            remote_agent_info.append(
                {'name': card.name, 'description': card.description}
            )
        return remote_agent_info
    
    async def send_message(self, agent_name: str, message: str) -> List[dict]:
        """向指定的远程Agent发送消息
        Args:
            agent_name: 远程Agent的名称
            message: 要发送的消息内容
        """
        if agent_name not in self.remote_agent_connections:
            raise ValueError(f'Agent {agent_name} not found')
        
        client = self.remote_agent_connections[agent_name]
        if not client:
            raise ValueError(f'Client not available for {agent_name}')
        
        messageId = str(uuid.uuid4())
        request: MessageSendParams = MessageSendParams(
            id=str(uuid.uuid4()),
            message=Message(
                role='user',
                parts=[TextPart(text=message)],
                messageId=messageId
            ),
            configuration=MessageSendConfiguration(
                acceptedOutputModes=['text', 'text/plain', 'image/png'],
            ),
        )
        response = await client.send_message(request, self.task_callback)
        # Extract text directly from response object
        if isinstance(response, Message):
            return [{"type": "message", "content": response.parts[0].text}]
        elif isinstance(response, Task):
            if response.artifacts and len(response.artifacts) > 0:
                for artifact in response.artifacts:
                    if artifact.parts and len(artifact.parts) > 0:
                        return [{"type": "task", "content": artifact.parts[0].root.text}]
            return [{"type": "task", "content": ""}]
        
        return []
    
    async def _send_messages(self, messages: list):
        return await self.client.chat.completions.create(
            model="qwen-max",
            messages=messages,
        )

    async def process_query(self, query: str) -> str:
        """Process a query using OpenAI and available tools"""
        agent_info = []
        for ra in self.list_remote_agents():
            agent_info.append(ra)
        #print("\navailable agents:", agent_info)

        messages = [
            {
                "role": "user",
                "content": """
You run in a loop of Thought, Action, Action Input, PAUSE, Observation.
At the end of the loop you output an Answer
Use Thought to describe your thoughts about the question you have been asked.
Use Action to run one of the actions available to you
use Action Input to indicate the input to the Action, which is a disassembly of the content for the user's question - then return PAUSE.
Observation will be the result of running those actions.

Your available actions are:
   
{agent_info}

Rules:
1- If the input is a greeting or a goodbye, respond directly in a friendly manner without using the Thought-Action loop.
2- Otherwise, follow the Thought-Action Input loop to find the best answer.
3- If you already have the answer to a part or the entire question, use your knowledge without relying on external actions.
4- If you need to execute more than one Action, do it on separate calls.
5- At the end, provide a final answer.

Some examples:

### 1
Question: 贵州茅台的收盘价是多少?
Thought: 我需要调用staock_Agent获取贵州茅台的收盘价
Action: staock_Agent
Action Input: 贵州茅台的收盘价是多少?

PAUSE

You will be called again with this:

Observation: 1700.

You then output: 
Final Answer: 贵州茅台的收盘价是1700.

Begin!

New input: {query}
""".format(agent_info=agent_info, query=query)
            }
        ]

        while True:
            # Initial OpenAI API call
            response = await self._send_messages(messages)
            message = response.choices[0].message
            print("\nmessage:", message.content)
            final_text = []
            final_text.append(message.content or "")

            final_answer_match = re.search(r'Final Answer:\s*(.*)', message.content)
            if final_answer_match:
                final_answer = final_answer_match.group(1)
                break
            
            messages.append({
                "role": "assistant",
                "content": message.content
            })

            action_match = re.search(r'Action:\s*(\w+)', message.content)
            action_input_match = re.search(r'Action Input:\s*(.*?)(?=\n|$)', message.content)
            
            if not action_match:
                raise ValueError(f"Could not find Action in response: {message.content}")
            if not action_input_match:
                raise ValueError(f"Could not find Action Input in response: {message.content}")
                
            agent = action_match.group(1)
            content = action_input_match.group(1).strip()

            print(f"\n需使用智能体{agent}")
            result = await self.send_message(agent, content)
            final_text.append(f"[Calling Agent {agent} with query {content}]")
            
            messages.append({
                "role": "user",
                "content": result[0].get('content', '')
            })

            # Get next response from OpenAI
            response = await self._send_messages(messages)
            
            message = response.choices[0].message
            if message.content:
                final_text.append(message.content)

        return "\n".join(final_text)

    async def chat_loop(self):
        """Run an interactive chat loop"""
        print("\nA2A Client Started!")
        print("Type your queries or 'quit' to exit.")
        
        while True:
            try:
                query = input("\nQuery: ").strip()
                if query.lower() == 'quit':
                    break
                response = await self.process_query(query)
                print("\n" + response)
            except Exception as e:
                print(f"\nError: {str(e)}")
    
    async def cleanup(self):
        """Clean up resources"""
        await self.exit_stack.aclose()

async def main():
    remote_agent_addresses = [
        "http://localhost:10000",
        "http://localhost:10001",
    ]
    # 创建异步HTTP客户端
    async with httpx.AsyncClient(timeout=50000) as client:
        # 初始化host agent
        host = HostAgent(
            remote_agent_addresses=remote_agent_addresses,
            http_client=client
        )
        await host.connect_to_server()

        try:
            await host.chat_loop()
        finally:
            await host.cleanup()

if __name__ == "__main__":
    asyncio.run(main())

然后是实现可以根据传入的股票代码获取股票信息的Remote Agent Server:

python 复制代码
# stock/agent_executor.py

import logging

from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.tasks import TaskUpdater
from a2a.types import (
    InternalError,
    InvalidParamsError,
    Part,
    Task,
    TaskState,
    TextPart,
    UnsupportedOperationError,
)
from a2a.utils import (
    new_agent_text_message,
    new_task,
)
from a2a.utils.errors import ServerError

from agent import StockAgent


logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class StockAgentExecutor(AgentExecutor):
    """股票助手AgentExecutor"""

    def __init__(self):
        self.agent = StockAgent()

    async def execute(
        self,
        context: RequestContext,
        event_queue: EventQueue,
    ) -> None:
        error = self._validate_request(context)
        if error:
            raise ServerError(error=InvalidParamsError())

        query = context.get_user_input()
        task = context.current_task
        if not task:
            task = new_task(context.message)
            event_queue.enqueue_event(task)
        updater = TaskUpdater(event_queue, task.id, task.context_id)
        try:
            response = await self.agent.invoke(query, task.context_id)
            is_task_complete = response['is_task_complete']
            require_user_input = response['require_user_input']
            print(response)
            if not is_task_complete and not require_user_input:
                await updater.update_status(
                    TaskState.working,
                    new_agent_text_message(
                        response['content'],
                        task.context_id,
                        task.id,
                    ),
                )
            elif require_user_input:
                await updater.update_status(
                    TaskState.input_required,
                    new_agent_text_message(
                        response['content'],
                        task.context_id,
                        task.id,
                    ),
                    final=True,
                )
            else:
                await updater.add_artifact(
                    [Part(root=TextPart(text=response['content']))],
                    name='conversion_result',
                )
                await updater.complete()

        except Exception as e:
            logger.error(f'An error occurred while streaming the response: {e}')
            raise ServerError(error=InternalError()) from e

    def _validate_request(self, context: RequestContext) -> bool:
        return False

    async def cancel(
        self, request: RequestContext, event_queue: EventQueue
    ) -> Task | None:
        raise ServerError(error=UnsupportedOperationError())
python 复制代码
# stock/main.py

import logging
import click
import httpx
import sys
import uvicorn

from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryPushNotificationConfigStore, BasePushNotificationSender, InMemoryTaskStore
from a2a.types import (
    AgentCapabilities,
    AgentCard,
    AgentSkill,
)

from agent import StockAgent
from agent_executor import StockAgentExecutor

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@click.command()
@click.option('--host', 'host', default='localhost')
@click.option('--port', 'port', default=10000)
def main(host, port):
    try:
        capabilities = AgentCapabilities(streaming=False, pushNotifications=True)
        skill = AgentSkill(
            id='股票信息',
            name='股票信息查询工具',
            description='可以查询股票的代码,名称,收盘价等信息',
            tags=['股票信息'],
            examples=['300750是哪只股票的代码?'],
        )
        agent_card = AgentCard(
            name='stock_Agent',
            description='可以查询股票的代码,名称,收盘价等信息',
            url=f'http://{host}:{port}/',
            version='1.0.0',
            defaultInputModes=StockAgent.SUPPORTED_CONTENT_TYPES,
            defaultOutputModes=StockAgent.SUPPORTED_CONTENT_TYPES,
            capabilities=capabilities,
            skills=[skill],
        )

        # --8<-- [start:DefaultRequestHandler]
        httpx_client = httpx.AsyncClient()
        push_notification_config_store = InMemoryPushNotificationConfigStore()
        push_notification_sender = BasePushNotificationSender(httpx_client,
                                config_store=push_notification_config_store)
        request_handler = DefaultRequestHandler(
            agent_executor=StockAgentExecutor(),
            task_store=InMemoryTaskStore(),
            push_config_store=push_notification_config_store,
            push_sender=push_notification_sender
        )
        server = A2AStarletteApplication(
            agent_card=agent_card, http_handler=request_handler
        )

        uvicorn.run(server.build(), host=host, port=port)

    except Exception as e:
        logger.error(f'An error occurred during server startup: {e}')
        sys.exit(1)

if __name__ == '__main__':
    main()

最后是数据分析Remote Agent Server:

python 复制代码
# data_analyze/agent_executor.py

import logging

from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.tasks import TaskUpdater
from a2a.types import (
    InternalError,
    InvalidParamsError,
    Part,
    Task,
    TaskState,
    TextPart,
    UnsupportedOperationError,
)
from a2a.utils import (
    new_agent_text_message,
    new_task,
)
from a2a.utils.errors import ServerError

from agent import DataAnalyzeAgent


logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class DataAnalyzeAgentExecutor(AgentExecutor):
    """数据分析助手AgentExecutor"""

    def __init__(self):
        self.agent = DataAnalyzeAgent()

    async def execute(
        self,
        context: RequestContext,
        event_queue: EventQueue,
    ) -> None:
        error = self._validate_request(context)
        if error:
            raise ServerError(error=InvalidParamsError())

        query = context.get_user_input()
        task = context.current_task
        if not task:
            task = new_task(context.message)
            event_queue.enqueue_event(task)
        updater = TaskUpdater(event_queue, task.id, task.context_id)
        try:
            response = await self.agent.invoke(query, task.context_id)
            is_task_complete = response['is_task_complete']
            require_user_input = response['require_user_input']
            print(response)
            if not is_task_complete and not require_user_input:
                await updater.update_status(
                    TaskState.working,
                    new_agent_text_message(
                        response['content'],
                        task.context_id,
                        task.id,
                    ),
                )
            elif require_user_input:
                await updater.update_status(
                    TaskState.input_required,
                    new_agent_text_message(
                        response['content'],
                        task.context_id,
                        task.id,
                    ),
                    final=True,
                )
            else:
                await updater.add_artifact(
                    [Part(root=TextPart(text=response['content']))],
                    name='conversion_result',
                )
                await updater.complete()

        except Exception as e:
            logger.error(f'An error occurred while streaming the response: {e}')
            raise ServerError(error=InternalError()) from e

    def _validate_request(self, context: RequestContext) -> bool:
        return False

    async def cancel(
        self, request: RequestContext, event_queue: EventQueue
    ) -> Task | None:
        raise ServerError(error=UnsupportedOperationError())
python 复制代码
# data_analyze/main.py

import logging
import click
import httpx
import sys
import uvicorn

from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryPushNotificationConfigStore, BasePushNotificationSender, InMemoryTaskStore
from a2a.types import (
    AgentCapabilities,
    AgentCard,
    AgentSkill,
)

from agent import DataAnalyzeAgent
from agent_executor import DataAnalyzeAgentExecutor

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@click.command()
@click.option('--host', 'host', default='localhost')
@click.option('--port', 'port', default=10001)
def main(host, port):
    try:
        capabilities = AgentCapabilities(streaming=False, pushNotifications=True)
        skill = AgentSkill(
            id='数据分析',
            name='数据分析工具',
            description='可以分析股票股价表现,财务情况等',
            tags=['数据分析'],
            examples=['对比一下 600600, 002461, 000729, 600573的股价表现和财务情况,哪家更值得投资'],
        )
        agent_card = AgentCard(
            name='data_analyze_Agent',
            description='可以分析股票股价表现,财务情况等',
            url=f'http://{host}:{port}/',
            version='1.0.0',
            defaultInputModes=DataAnalyzeAgent.SUPPORTED_CONTENT_TYPES,
            defaultOutputModes=DataAnalyzeAgent.SUPPORTED_CONTENT_TYPES,
            capabilities=capabilities,
            skills=[skill],
        )

        # --8<-- [start:DefaultRequestHandler]
        httpx_client = httpx.AsyncClient()
        push_notification_config_store = InMemoryPushNotificationConfigStore()
        push_notification_sender = BasePushNotificationSender(httpx_client,
                                config_store=push_notification_config_store)
        request_handler = DefaultRequestHandler(
            agent_executor=DataAnalyzeAgentExecutor(),
            task_store=InMemoryTaskStore(),
            push_config_store=push_notification_config_store,
            push_sender=push_notification_sender
        )
        server = A2AStarletteApplication(
            agent_card=agent_card, http_handler=request_handler
        )

        uvicorn.run(server.build(), host=host, port=port)

    except Exception as e:
        logger.error(f'An error occurred during server startup: {e}')
        sys.exit(1)

if __name__ == '__main__':
    main()

用python执行他们的启动文件。下面测试一下:

Query: 查询002468的股票名称是什么?分析一下该股票的市场行情和财务情况,判断是否值得投资

执行结果:

message: Thought: 首先,我需要通过stock_Agent来查询002468的股票名称。之后,根据得到的信息,再使用data_analyze_Agent对该股票的市场行情和财务情况进行分析,以判断其投资价值。

Action: stock_Agent

Action Input: 002468的股票名称是什么?

PAUSE

需使用智能体stock_Agent

message: Thought: 现在我知道了002468对应的股票名称是申通快递。接下来,我将使用data_analyze_Agent来分析申通快递的市场行情和财务情况,以评估其投资价值。

Action: data_analyze_Agent

Action Input: 分析申通快递(002468)的市场行情和财务情况,判断是否值得投资

PAUSE

需使用智能体data_analyze_Agent

message: Final Answer: 根据对申通快递(002468)的市场行情和财务情况的综合分析,可以得出以下结论:

财务分析

  • **收入表现**:营业总收入为471.69亿元,同比增长15.26%,季度环比增长14.73%。收入增长稳健,显示业务扩张良好。

  • **盈利能力**:净利润为10.40亿元,同比增长205.24%,每股收益为0.69元。销售毛利率为6.00%,净资产收益率为11.19%。净利润大幅增长超过200%,盈利能力显著改善;ROE达到11.19%,处于较好水平。

  • **财务健康状况**:每股净资产为6.41元,每股经营现金流量为2.58元。资产质量良好,经营现金流充裕,财务结构相对稳健。

市场行情分析

  • **价格表现**:起始价格为8.27元,结束价格为11.23元,区间涨跌幅为+35.79%。近期股价表现强劲,涨幅显著。

  • **风险指标**:最大回撤为37.79%,年化波动率为36.46%。股票波动性较高,最大回撤较大,显示投资风险相对较高。

综合投资判断

  • **积极因素**:

  • 业绩增长强劲,收入和净利润均实现双位数增长,特别是净利润同比增长205%,盈利能力大幅提升。

  • 行业地位稳固,受益于电商发展和物流需求增长。

  • 估值相对合理,基于当前财务数据和股价表现,估值有一定吸引力。

  • 现金流健康,经营现金流充裕,支持业务发展和分红能力。

  • **风险因素**:

  • 行业竞争激烈,物流行业竞争白热化,价格战可能影响利润率。

  • 毛利率偏低,6%的毛利率在物流行业中处于较低水平。

  • 股价波动大,年化波动率36.46%,最大回撤37.79%,投资风险较高。

  • 宏观经济影响,物流行业对经济周期敏感,经济放缓可能影响业务增长。

投资建议

  • **适合风险承受能力较强的投资者**:由于股价波动较大,建议风险偏好较高的投资者考虑。

  • **关注行业竞争格局**:需要密切关注行业价格战对公司利润率的影响。

  • **分批建仓策略**:鉴于较大回撤风险,建议采用分批买入策略。

  • **长期持有视角**:物流行业具有长期增长潜力,适合中长期投资。

**总体评级:谨慎乐观**

申通快递在财务改善方面表现突出,但行业竞争和股价波动风险需要谨慎对待。对于能够承受一定波动性的投资者,当前价位具有一定投资价值。

Final Answer: 根据对申通快递(002468)的市场行情和财务情况的综合分析,可以得出以下结论:

财务分析

  • **收入表现**:营业总收入为471.69亿元,同比增长15.26%,季度环比增长14.73%。收入增长稳健,显示业务扩张良好。

  • **盈利能力**:净利润为10.40亿元,同比增长205.24%,每股收益为0.69元。销售毛利率为6.00%,净资产收益率为11.19%。净利润大幅增长超过200%,盈利能力显著改善;ROE达到11.19%,处于较好水平。

  • **财务健康状况**:每股净资产为6.41元,每股经营现金流量为2.58元。资产质量良好,经营现金流充裕,财务结构相对稳健。

市场行情分析

  • **价格表现**:起始价格为8.27元,结束价格为11.23元,区间涨跌幅为+35.79%。近期股价表现强劲,涨幅显著。

  • **风险指标**:最大回撤为37.79%,年化波动率为36.46%。股票波动性较高,最大回撤较大,显示投资风险相对较高。

综合投资判断

  • **积极因素**:

  • 业绩增长强劲,收入和净利润均实现双位数增长,特别是净利润同比增长205%,盈利能力大幅提升。

  • 行业地位稳固,受益于电商发展和物流需求增长。

  • 估值相对合理,基于当前财务数据和股价表现,估值有一定吸引力。

  • 现金流健康,经营现金流充裕,支持业务发展和分红能力。

  • **风险因素**:

  • 行业竞争激烈,物流行业竞争白热化,价格战可能影响利润率。

  • 毛利率偏低,6%的毛利率在物流行业中处于较低水平。

  • 股价波动大,年化波动率36.46%,最大回撤37.79%,投资风险较高。

  • 宏观经济影响,物流行业对经济周期敏感,经济放缓可能影响业务增长。

投资建议

  • **适合风险承受能力较强的投资者**:由于股价波动较大,建议风险偏好较高的投资者考虑。

  • **关注行业竞争格局**:需要密切关注行业价格战对公司利润率的影响。

  • **分批建仓策略**:鉴于较大回撤风险,建议采用分批买入策略。

  • **长期持有视角**:物流行业具有长期增长潜力,适合中长期投资。

**总体评级:谨慎乐观**

申通快递在财务改善方面表现突出,但行业竞争和股价波动风险需要谨慎对待。对于能够承受一定波动性的投资者,当前价位具有一定投资价值。

相关推荐
凡泰极客科技3 小时前
新浪财经专访凡泰极客梁启鸿:金融App的AI落地应避哪些坑
人工智能·金融
雷焰财经4 小时前
大宗商品与市场波动:2026年开局的油价、贵金属与股市联动
金融
2501_947908204 小时前
范建峰携手安盛投资 助力普惠金融惠及更多民生领域
大数据·人工智能·金融
少云清5 小时前
【金融项目实战】5_功能测试 _业务流程测试
功能测试·金融
慧都小项7 小时前
金融文档的“自主可控”:Python下实现Word到ODT的转换
python·金融·word
机器学习之心1 天前
金融时间序列预测全流程框架:从SHAP特征选择到智能算法优化深度学习预测模型,核心三章实验已完成,尚未发表,期待有缘人!
人工智能·深度学习·金融
TM1Club2 天前
AI驱动的预测:新的竞争优势
大数据·人工智能·经验分享·金融·数据分析·自动化
2501_947908202 天前
中金金融认证中心(CFCA)电子签约产品:安心签
金融
AIFQuant2 天前
如何利用免费股票 API 构建量化交易策略:实战分享
开发语言·python·websocket·金融·restful