跳到主要内容

第 3 章 · 数据层、厂商路由与防幻觉

本章讲数据从哪来、怎么在多个厂商间路由、以及框架花了最大力气做的一件事:不让 LLM 凭空编数字。这是整个项目工程含量最高的一支。

3.1 全景:工具、厂商、路由三层

agent 想要数据
│ 调工具 get_stock_data / get_news / get_fundamentals ...

[抽象工具层] agent_utils.py 汇总所有工具的公开入口


[厂商路由] interface.route_to_vendor(method, ...)
│ 按 category → 配置的厂商链 → 逐个试

[厂商实现] yfinance / alpha_vantage / fred / polymarket

工具按类别组织(interface.py:36-78):核心行情、技术指标、基本面、新闻、宏观(FRED)、预测市场(Polymarket)。每个方法映射到多个厂商实现(VENDOR_METHODS, interface.py:95-144)。

3.2 机制一:厂商路由——显式链,失败要响

它要解决的小问题: 同一份数据可能有多个来源(yfinance、Alpha Vantage)。天真做法是“主厂商失败就自动换下一个”,但这带来一个隐患:用户明明只选了 yfinance,却拿到了 Alpha Vantage 的数据,导致跨厂商数字不一致。

本项目的取舍(#988/#289): 配置的厂商列表本身就是链,绝不 fallback 到你没选的厂商。 想要多厂商兜底就自己列,如 data_vendors="yfinance,alpha_vantage"。核心逻辑(interface.py:168):

route_to_vendor(method):
vendor_chain = 配置里显式列的厂商(过滤掉不支持该 method 的)
for vendor in vendor_chain:
try: return impl(*args)
except VendorRateLimitError: 记日志,试下一个
except VendorNotConfiguredError: 记第一个错,试下一个
except NoMarketDataError: 记为 last_no_data,试下一个
except Exception: 记日志+第一个错,试下一个 ← 绝不静默吞掉(#989)

两种收尾很讲究:

  1. 有厂商明确说“没数据” → 返回一个指导性哨兵字符串 NO_DATA_AVAILABLE: ...(interface.py:242-247),明确告诉 agent“这符号可能无效/退市/未覆盖/数据陈旧,别估别编,就报不可用”。这比返回空字符串强——空字符串会诱导 LLM 编一个价格。
  2. 可选类别(宏观、预测市场)出错 → 降级成 DATA_UNAVAILABLE 哨兵而非中断整个 run(OPTIONAL_CATEGORIES, interface.py:92interface.py:254-259)。核心数据(行情/基本面/新闻)出错则大声抛出,不许静默。

配置在 default_config.py:133-144:类别级 data_vendors + 工具级 tool_vendors(后者优先)。

3.3 机制二:符号归一化——先翻译成 Yahoo 的“方言”

它要解决的小问题: 用户/券商习惯写 XAUUSD(黄金)、EURUSD(外汇)、BTCUSD(加密)、SPX500(指数 CFD),但 Yahoo Finance 要的是 GC=FEURUSD=XBTC-USD^GSPC。直接把券商符号丢给 Yahoo 会返回空——然后 LLM 就可能围绕空结果编个价格(issue #781)。

思路: 一个纯语法(无网络调用)的 normalize_symbol(symbol_utils.py:104),按优先级四步翻译:

1. 显式别名表(金属/能源/指数CFD): XAUUSD→GC=F, SPX500→^GSPC ...
2. 加密规则: 已知币种 + USD/USDT/USDC → BASE-USD
3. 外汇规则: 六字母且两半都是 ISO 币种 → PAIR=X
4. 都不匹配: 原样大写返回(普通股票/ETF/Yahoo 原生符号)

归一化在多处复用:拉行情、算 alpha 收益(trading_graph.py:272)、解析标的身份(agent_utils.py:96)——保证“定身份、拉价格、算收益”打的是同一个 instrument(issue #983/#984)。

3.4 机制三:标的身份——运行开始就钉死“这是哪家公司”

它要解决的小问题(issue #814): 没有 ground-truth 公司名时,市场分析师会把价格走势 pattern-match 成某个行业的叙事,然后编造一个身份,这个错误身份接着往下游所有 agent 传染。

思路: 运行一开始就用 yfinance 做一次确定性身份解析,注入所有 agent。

resolve_instrument_context(ticker, asset_type) ← trading_graph.py:336

├─ resolve_instrument_identity(ticker) ← agent_utils.py:79
│ yfinance 拉 longName/sector/industry/exchange,lru_cache 缓存
│ 失败就返回 {} —— fail-open,绝不因身份查不到就阻断分析

build_instrument_context(...) ← agent_utils.py:122
生成一段话注入 state.instrument_context:
"要分析的是 `TICKER`,在每次工具调用/报告里都用这个精确符号...
已解析身份: 公司 X; 行业 Y/Z; 交易所 W。
除非工具结果明确推翻,不许换成别的公司或符号。"

这段 context 通过 get_instrument_context_from_state(agent_utils.py:172)被每个 agent 读取。fail-open 设计很关键:yfinance 限流/不认识都只返回 {},退化成“仅符号”的 context,绝不在分析开始前就挂掉。

3.5 机制四:核验快照——精确数字的唯一真值

它要解决的小问题(issue #830): 市场分析师会编精确数字——引用一个不存在的 Bollinger 带值、或“历史验证过的反弹位”,而底层数据根本不支持。

思路: 提供一个完全确定性、无 LLM 参与的核验快照工具 build_verified_market_snapshot(market_data_validator.py:62),算出:分析日当天或之前的最新 OHLCV 行、一组固定的常用指标、最近若干日收盘。渲染成 markdown 表交给分析师,并在 prompt 里命令它“把这当精确数字的唯一真值,别的工具冲突就报差异,不许自己调和”。

防前视(look-ahead)在这里是双保险:load_ohlcv 已经过滤未来行,核验路径又防御性地再切一次 df[df["Date"] <= curr_date](market_data_validator.py:41-44)——因为这是核验路径,不信任输入已被预过滤。

这个工具被绑进市场分析师的工具集且被 prompt 强制调用,所以它必须在图里可执行,否则调用失败模型会报“不可用”(trading_graph.py:196-201 的注释点明了这点)。

3.6 机制五:grounded 情绪分析——先喂真数据,再让它开口

它要解决的小问题(issue #557/#796): 老的“社交媒体分析师”prompt 要求分析社交媒体,但手里只有 Yahoo 新闻这一个工具——于是 LLM 在 prompt 压力下编造 Reddit/X/StockTwits 内容

思路(sentiment_analyst.py:1-24 的模块注释说得很清楚): 改名情绪分析师,不用 tool-calling,改成在调 LLM 前预取三个数据源塞进 prompt:

内容取数
新闻Yahoo Finance 头条(机构视角)get_news.func(...)
StockTwits散户帖 + 用户自标 Bullish/Bearishfetch_stocktwits_messages
Redditr/wallstreetbets、r/stocks、r/investingfetch_reddit_posts

每个 fetcher 优雅降级(出错返回占位字符串而非抛异常),所以 LLM 从第 0 轮就看到东西——要么真数据要么明确占位,没有“空着诱导它编”的机会。输出走结构化(SentimentReport:band + score + confidence + narrative,schemas.py:273)。

3.7 小结:四道防幻觉闸门

闸门对付什么关键符号issue
符号归一化券商符号→Yahoo,避免空结果诱导编价normalize_symbol#781
身份解析防止把价格走势编成错误公司resolve_instrument_identity#814
核验快照精确数字必须有据build_verified_market_snapshot#830
grounded 情绪别编社交内容create_sentiment_analyst(预取数据)#557/#796
NO_DATA 哨兵没数据就说没有,别估route_to_vendor 收尾#988/#289

3.8 代码地图(本章)

主题文件符号
厂商路由tradingagents/dataflows/interface.pyroute_to_vendorVENDOR_METHODSOPTIONAL_CATEGORIES
符号归一化tradingagents/dataflows/symbol_utils.pynormalize_symbolcrypto_base_ALIASES
核验快照tradingagents/dataflows/market_data_validator.pybuild_verified_market_snapshot_verified_rows
标的身份tradingagents/agents/utils/agent_utils.pyresolve_instrument_identitybuild_instrument_context
grounded 情绪tradingagents/agents/analysts/sentiment_analyst.pycreate_sentiment_analyst
错误类型tradingagents/dataflows/errors.pyNoMarketDataErrorVendorRateLimitErrorVendorNotConfiguredError