引言:为什么自然语言股票查询是量化分析的“刚需”?
在金融投资中,“快速获取股票信息”是最基础的需求。无论是资深分析师还是普通投资者,都曾遇到过这样的场景:
- 听到“宁德时代”想查询其代码,却要在行情软件中手动输入拼音首字母搜索;
- 看到代码“600519”想知道对应公司名称,却要记忆“沪市6开头、深市0/3开头”的规则;
- 想了解“新能源板块的龙头股票有哪些”,却要逐个筛选几十支股票的行业标签。
这些问题的核心在于:传统查询方式需要用户适应机器的“语法规则”(如代码格式、精确名称),而非机器理解人类的自然语言。
本文将系统讲解如何用LangGraph Agent(自然语言理解框架)+ AKShare(金融数据工具)构建一套自然语言股票查询系统,实现“说句话就能查股票”的体验。从数据获取到Agent决策,从代码实战到部署优化,全方位覆盖技术细节与最佳实践。
一、核心技术栈解析:从“数据”到“理解”的全链路
构建自然语言股票查询系统需要四大技术模块协同工作,形成完整的“输入-处理-输出”链路:
graph TD
A[用户自然语言输入] --> B[LangGraph Agent(理解与决策)]
B --> C[工具函数(查询逻辑)]
C --> D[AKShare(数据来源)]
D --> C
C --> B
B --> E[自然语言输出结果]
1. 数据层:AKShare——免费实时的金融数据源
AKShare是Python生态中最成熟的免费金融数据接口库,支持沪深A股、港股、美股等全球市场的实时行情数据。其核心优势在于:
- 实时性:能获取当日最新股价、涨跌幅等动态数据;
- 结构化:返回pandas DataFrame格式,便于筛选和处理;
- 覆盖广:包含股票代码、名称、行业分类等元数据,满足查询需求。
基础代码示例:获取创业板实时数据
import akshare as ak
# 获取创业板所有股票的实时数据(代码、名称、最新价等)
df = ak.stock_cy_a_spot_em() # em代表东方财富数据源
print(df[["代码", "名称", "最新价", "涨跌幅"]].head())
输出结果示例:
代码 | 名称 | 最新价 | 涨跌幅 |
---|---|---|---|
300750 | 宁德时代 | 210.50 | +1.20% |
300059 | 东方财富 | 14.80 | -0.54% |
300124 | 汇川技术 | 65.30 | +2.15% |
2. 工具层:函数封装——将查询逻辑转化为“可调用工具”
需要将股票查询逻辑封装为标准化函数,供Agent调用。工具函数需满足:
- 明确的输入参数(如
code
或name
); - 结构化的输出(如字典或列表,便于LLM解析);
- 完善的异常处理(如未找到股票时返回友好提示)。
工具函数封装示例:
from langchain_core.tools import tool
import pandas as pd
# 全局存储股票数据(避免重复调用AKShare,提升效率)
stock_data = ak.stock_cy_a_spot_em() # 初始化时加载一次
@tool
def get_stock_info(code: str = None, name: str = None) -> str:
"""
根据股票代码或名称查询详细信息(支持模糊搜索)
参数:
code: 股票代码(如"300750")
name: 股票名称(如"宁德时代")
返回:
股票信息字典列表,包含代码、名称、最新价等
"""
global stock_data
result = None
try:
# 优先按代码查询(精确匹配)
if code:
result = stock_data[stock_data["代码"].str.contains(code.strip())]
# 按名称查询(模糊匹配)
elif name:
result = stock_data[stock_data["名称"].str.contains(name.strip())]
# 处理无结果的情况
if result.empty:
return f"未找到代码为{
code}或名称含{
name}的股票"
# 转换为字典列表,便于LLM解析
return result[["代码", "名称", "最新价", "涨跌幅", "所属行业"]].to_dict(orient="records")
except Exception as e:
return f"查询出错:{
str(e)}"
关键设计点:
- 使用
@tool
装饰器:这是LangChain/LangGraph的标准写法,能自动生成工具描述,供LLM理解其功能; - 支持模糊搜索:通过
str.contains()
实现“输入部分名称/代码即可查询”(如输入“宁德”可匹配“宁德时代”); - 全局数据缓存:初始化时加载一次数据,避免重复调用AKShare导致的延迟和限频。
3. 理解层:LLM大模型——实现自然语言到指令的转换
大模型是系统的“大脑”,负责将用户的自然语言(如“宁德时代的代码是多少”)解析为工具函数的调用参数(如name="宁德时代"
)。核心要求是:支持Function Calling功能(即能识别何时需要调用工具,并生成规范的调用格式)。
推荐模型:
- DeepSeek-Chat(V3):国产模型中Function Calling支持较好,对中文处理更精准;
- GPT-3.5-turbo/GPT-4:生态成熟,但中文金融术语理解略逊于国产模型;
- 禁止使用:DeepSeek-R1、LLaMA等纯推理模型(无Function Calling能力,无法调用工具)。
4. 决策层:LangGraph——控制工具调用的工作流
LangGraph是构建Agent的框架,负责协调“大模型理解→工具调用→结果整理”的全流程。其核心价值在于:
- 支持循环工作流:复杂查询可多轮调用工具(如“先查宁德时代代码,再查其最新价”);
- 状态管理:保存对话历史,支持上下文关联查询(如“它的涨跌幅是多少”中的“它”指代前文的股票);
- 条件分支:根据工具返回结果决定下一步操作(继续调用工具或直接回答)。
二、LangGraph Agent实战:两种实现方式对比
LangGraph提供了两种构建Agent的模式,分别适合不同场景:手动实现(灵活度高)和预构建Agent(快速开发)。
1. 方式一:手动实现Function Calling(适合深度定制)
手动构建工作流,需定义节点函数和条件分支,步骤更繁琐但可精确控制每一步逻辑。
步骤1:定义状态结构
状态用于存储对话历史和中间结果,是Agent决策的依据:
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from pydantic import BaseModel, Field
from typing import List, Annotated
# 定义状态结构:存储消息列表
class AgentState(BaseModel):
messages: Annotated[List, add_messages] = Field(default_factory=list)
步骤2:绑定工具到LLM
将大模型与工具函数关联,使LLM知道“可以调用哪些工具”:
from langchain_community.chat_models import ChatDeepSeek
import os
# 初始化DeepSeek模型(需设置API密钥)
os.environ["DEEPSEEK_API_KEY"] = "你的API密钥"
llm = ChatDeepSeek(model="deepseek-chat", temperature=0) # temperature=0确保输出稳定
# 将工具绑定到LLM(大模型将知道如何调用这些工具)
llm_with_tools = llm.bind_tools([get_stock_info])
步骤3:定义节点函数
节点是工作流的“步骤”,包括“LLM决策节点”和“工具调用节点”:
# 节点1:LLM决策节点(判断是否需要调用工具)
def llm_decide_node(state: AgentState):
# 调用绑定工具的LLM,传入对话历史
response = llm_with_tools.invoke(state["messages"])
# 将LLM的响应添加到状态中
return {
"messages": [response]}
# 节点2:工具调用节点(执行工具并获取结果)
def tool_call_node(state: AgentState):
# 获取LLM最新的响应(包含工具调用指令)
last_message = state["messages"][-1]
tool_results = []
# 遍历所有工具调用指令
for tool_call in last_message.tool_calls:
# 调用对应的工具函数(此处仅get_stock_info一个工具)
if tool_call["name"] == "get_stock_info":
result = get_stock_info.invoke(tool_call["args"])
# 将工具结果包装为ToolMessage
tool_results.append(ToolMessage(
content=str(result),
tool_call_id=tool_call["id"]
))
# 将工具结果添加到状态中
return {
"messages": tool_results}
步骤4:构建工作流
通过状态图定义节点之间的连接关系,实现“LLM决策→工具调用→结果整理”的循环:
# 创建状态图
graph = StateGraph(AgentState)
# 添加节点
graph.add_node("llm_decide", llm_decide_node) # LLM决策节点
graph.add_node("tool_call", tool_call_node) # 工具调用节点
# 定义流程:从START开始,先进入LLM决策节点
graph.add_edge(START, "llm_decide")
# 条件分支:LLM决策后,若需要调用工具则进入tool_call节点,否则直接结束
def should_call_tool(state: AgentState):
last_message = state["messages"][-1]
# 检查LLM的响应中是否包含工具调用指令
if last_message.tool_calls:
return "tool_call" # 需要调用工具
else:
return END # 无需调用工具,流程结束
graph.add_conditional_edges(
"llm_decide", # 源节点
should_call_tool, # 条件判断函数
{
"tool_call": "tool_call", END: END} # 分支目标
)
# 工具调用后,返回LLM决策节点继续处理结果
graph.add_edge("tool_call", "llm_decide")
# 编译为可运行的Agent
agent = graph.compile()
步骤5:测试手动构建的Agent
# 测试查询:"宁德时代的股票代码是什么?"
messages = [HumanMessage(content="宁德时代的股票代码是什么?")]
result = agent.invoke({
"messages": messages})
# 输出最终回复(从状态的消息列表中获取)
for msg in result["messages"]:
if isinstance(msg, AIMessage):
print("Agent回复:", msg.content)
输出结果:
Agent回复:宁德时代的股票代码是300750。其最新价为210.50元,涨跌幅为+1.20%,所属行业为电力设备。
2. 方式二:预构建Agent(适合快速开发)
LangGraph提供create_react_agent
函数,封装了上述工作流,一行代码即可创建Agent,适合追求效率的场景。
代码示例:
from langgraph.prebuilt import create_react_agent
# 一行代码创建Agent(自动包含LLM决策、工具调用、结果整理逻辑)
agent = create_react_agent(llm, tools=[get_stock_info])
# 测试查询:"代码300750对应的股票名称是什么?"
response = agent.invoke({
"messages": [HumanMessage(content="代码300750对应的股票名称是什么?")]
})
# 提取并打印回复
print("Agent回复:", response["messages"][-1].content)
输出结果:
Agent回复:代码300750对应的股票名称是宁德时代,其最新价为210.50元,涨跌幅为+1.20%,所属行业为电力设备。
3. 两种实现方式的对比
实现方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
手动构建 | 可定制工作流细节&# |