用于比较实时股票指标的手动滚动的LangChain工具循环
逐步构建一个小型 LangChain 股票对比助手,了解让大语言模型能够请求实时数据而非凭猜测工作的创建、绑定、调用与执行流程。
语言模型仅知晓其训练数据中的内容,因此询问它今天的股价只会得到一个有根据的猜测,而非真实数据。对于任何金融相关问题而言,这都是个严重的问题。Tool calling功能通过让模型调用你的代码来获取实时数据并基于结果进行分析,从而解决这一问题。本示例将构建一个简洁的助手,它能通过股价和市盈率来比较两只股票,并在此过程中清晰展示LangChain工具调用工作流的每个阶段:创建、绑定、调用和执行。
为何没有工具的模型只能猜测
想象一下,一名店员对所有产品和政策都了如指掌,但却无法访问库存系统。如果你询问是否有中号夹克在售,他只能根据上周的情况给出有根据的回答。这就像单独使用的LLM:表达能力很强,却对现实情况一无所知。
将条形码扫描器交给助手,他们就能通过验证而非猜测来获取信息。在 LangChain 中,扫描器是一种工具:即模型可以要求运行的普通函数,比如查询价格、执行数据库操作或调用 API。工具调用机制让模型能够决定何时使用扫描器、扫描哪些内容,以及如何处理返回的结果。
四阶段循环
工具调用(通常也称为函数调用)始终遵循相同的流程:
- 创建工具。编写一个具有描述性名称、带类型参数的 Python 函数,并附上说明其功能的文档字符串。
- 将其绑定到模型上。在聊天模型上调用
.bind_tools(),这样模型就能知道有哪些工具以及它们需要哪些参数。
get_stock_price 函数,参数为 ticker='MSFT'”这样的结构化请求,而非最终答案。关键在于,是模型而非你的代码来决定何时需要使用工具。像“2+2等于多少?”这样的问题可以直接得到答案,而“比较MSFT和AAPL”则可能在同一响应中触发两次工具调用。模型仅负责发起这些调用,实际执行仍需由你来完成。关于各种框架如何自动化这一流程,请参阅构建代理循环后LangChain究竟能自动化哪些环节。
第一步:首次运行时安装依赖项
该脚本首先检查所需的包,安装任何缺失的包,这样人们无需额外执行 pip install 即可运行它。它会将每个包名中的连字符替换为下划线,从而转换为对应的导入名称(例如 langchain-groq 变为 langchain_groq),若无法转换则通过当前解释器调用 pip。
import sys
import subprocess
required_packages = ["langchain-groq", "langchain-core", "yfinance"]
for package in required_packages:
try:
__import__(package.replace("-", "_"))
except ImportError:
print(f"📦 Package '{package}' not found. Installing now...")
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
这种方式适合演示用途,但实际项目中应将依赖项指定在 requirements.txt 或 pyproject.toml 中;运行时安装会导致构建结果不可复现。
步骤 2:提供 API 密钥
该模型在 Groq 上运行,因此 Groq 客户端需要在环境中拥有 API 密钥。演示中以占位符的形式直接给出了该密钥:
import os
os.environ["GROQ_API_KEY"] = "Your-API"
真实的 API 密钥应存储在不会被纳入版本控制的 .env 文件中或密钥管理工具中,绝不能放在已提交的代码里。
步骤3:定义股票数据工具
该工具使用yfinance来查询股票代码,获取历史数据中某一天的最新收盘价,并从该股票的资料中读取市值和滚动市盈率:
from langchain_core.tools import tool
import yfinance as yf
@tool
def get_stock_price(ticker: str) -> str:
"""Fetches the current stock price and key statistics for a given ticker symbol."""
try:
stock = yf.Ticker(ticker)
todays_data = stock.history(period='1d')
if todays_data.empty:
return f"Could not find data for ticker {ticker}."
price = todays_data['Close'].iloc[-1]
info = stock.info
market_cap = info.get('marketCap', 'N/A')
pe_ratio = info.get('trailingPE', 'N/A')
return f"{ticker} Current Price: ${price:.2f}, Market Cap: {market_cap}, P/E Ratio: {pe_ratio}"
except Exception as e:
return f"Error fetching data for {ticker}: {str(e)}"
有三个特点使得这个函数成为一款出色的工具:
@tool装饰器将函数转换为LangChain工具,并从其类型提示中生成输入结构。- 文档字符串并非装饰元素。模型会结合函数名称与文档内容来判断何时使用该工具,因此文档中应明确说明工具的返回结果。
- 错误以描述性字符串的形式返回,而非抛出异常。遇到未知股票代码或请求失败时,系统会返回模型能够理解的提示信息(如“无法找到数据”),而不会抛出导致整个流程中断的异常。
请注意,yfinance 是一个非官方的 Yahoo Finance 封装工具:数据报价可能会出现延迟,某些字段也可能缺失,因此会使用 'N/A' 作为替代值。
第4步:创建模型并绑定工具
接下来需要创建聊天模型,并将工具列表关联到该模型上:
from langchain_groq import ChatGroq
llm = ChatGroq(
model="openai/gpt-oss-120b",
temperature=0
)
tools = [get_stock_price]
llm_with_tools = llm.bind_tools(tools)
bind_tools() 会在每次请求中将工具的名称、描述及参数结构发送给模型,以便模型知晓可以调用哪些功能。需要注意的是,未绑定的 llm 也会被保留下来,稍后会用于生成最终总结。模型名称反映了撰写本文时 Groq 提供的模型情况,因此在运行之前请查看该服务提供商当前的模型列表。
将 temperature=0 设置为默认值可使输出更加集中且一致,但这只是降低随机性,并不能保证每次的响应完全相同。
第5步:构建提示词链
提示词模板会提供系统指令,并将用户的问题嵌入到人工输入信息中。随后,管道运算符会将提示词与具备工具认知能力的模型结合为一个可运行的整体,类似于 Unix 管道:
from langchain_core.prompts import ChatPromptTemplate
from IPython.display import display, Markdown
prompt = ChatPromptTemplate.from_messages([
("system", "You are an expert financial analyst. Use the tools provided to pull real-time data before comparing or concluding."),
("human", "{input}")
])
chain = prompt | llm_with_tools
系统指令在这里起着关键作用:它指示模型在进行任何比较之前先使用工具获取数据。如果没有这一指令,模型可能会依据过时的训练数据来回答。
第6步:运行循环并处理多次工具调用
主程序块负责整合所有环节。它启动处理流程,检查响应中是否包含tool_calls,执行每个请求的查询,收集输出结果,最后让普通模型根据这些数据撰写比较内容。如果未请求使用任何工具,则直接输出模型的回答:
if __name__ == "__main__":
query = "Compare the current stock price and P/E ratio of Microsoft (MSFT) AND Apple (AAPL). Which one looks cheaper based on P/E?"
print(f"🚀 Invoking Financial Pipeline with query: '{query}'\n")
# 1. Ask the model what tools it wants to use
ai_message = chain.invoke({"input": query})
# 2. Check if the model requested tool use
if ai_message.tool_calls:
print(f"🛠️ Model requesting {len(ai_message.tool_calls)} real-time tool lookups...\n")
tool_outputs = []
# 3. Execute ALL generated tool calls
for tool_call in ai_message.tool_calls:
if tool_call["name"] == "get_stock_price":
ticker_symbol = tool_call["args"]["ticker"]
print(f" -> Executing tool lookup for: {ticker_symbol}")
result = get_stock_price.invoke(tool_call["args"])
print(f" [Tool Output] {result}")
tool_outputs.append(result)
# 4. Supply the full collective data back to the LLM
summary_prompt = f"""
User Query: {query}
Real-time Data Harvested: {'; '.join(tool_outputs)}
Synthesize a final response evaluating which asset looks cheaper.
"""
final_answer = llm.invoke(summary_prompt)
print("\n--- Final Analysis Output ---")
display(Markdown(final_answer.content))
else:
print("\n--- Final Analysis Output ---")
print(ai_message.content)
对于同时涉及微软和苹果的问题,模型通常会在一个响应中返回两次工具调用,每个股票代码对应一次。循环会先执行所有这些调用,之后才会继续处理,因此最终步骤会一次性看到两组数值。模型要求并行执行这些调用,但这段代码是依次执行的;对于响应速度较慢的API,可以改为并发执行。
当该功能正常运行后,有两点改进值得了解。首先,循环中的名称检查可用于在多个工具之间进行选择;使用将工具名称映射到工具对象的字典,其扩展性优于一系列if语句。其次,此版本通过构建新的文本提示来传递结果。更符合LangChain惯用方式的做法是将每个结果作为携带对应tool_call_id的ToolMessage附加到对话中,然后再调用与工具绑定的模型,这样既能保留完整的交互记录,也能在初始结果不足时让模型请求进一步调用。
扩展助手是逐步进行的:通过相同的bind_tools()调用即可添加如get_financial_news或calculate_valuation这样的工具,模型则会根据这些工具的描述来选择使用哪个工具。
关键要点
- 工具是一个具有明确名称、类型提示和文档字符串的函数;
@tool装饰器负责处理其余工作。 .bind_tools()通过在每个请求中描述函数,将它们与模型连接起来。- 模型仅要求调用工具。你的代码执行这些工具并返回结果,这种控制方式是优势而非限制。
- 将工具返回的错误以易读的字符串形式呈现,这样一次错误的查询不会导致整个流程终止。
- 先从一个工具和一个循环开始,然后根据需要添加更多工具、基于消息的结果传递机制以及并发处理功能。
无论模型需要的是天气、库存信息,还是CRM记录或自定义数据库中的数据,都适用同样的模式,这样助手就能基于实时系统进行推理,而非依赖静态的快照。