最近在做股票 Agent 接入时,我遇到的最大问题不是“数据拿不到”,而是不同数据源对同一种情况给出了完全不同的返回。
同样是查询某只股票最近一个交易日的数据,有的接口返回空数组,有的返回空 DataFrame,有的直接抛异常;周末查询时,有的自动回退到周五,有的仍然保留当天日期;权限不足和当天数据尚未更新,也可能都表现为“没有数据”。
这些差异对普通脚本还算好处理,但直接交给 Agent 后,很容易出现三类错误:把空数据解释成股票停牌、把上一交易日数据说成今天、把权限失败写成“当天没有龙虎榜”。
所以在选择数据源之前,我更建议先补一个统一返回层。
不要把上游返回直接暴露给 Agent
Tushare、AkShare、商业行情 API 和自建数据库的调用方式不同,但 Agent 真正需要的不是原始 DataFrame 或任意 JSON,而是一组语义稳定的工具。
我给所有数据工具统一了下面几个字段:
type ToolStatus =
| "ok"
| "empty"
| "not_ready"
| "forbidden"
| "rate_limited"
| "error";
interface StockToolResult<T> {
status: ToolStatus;
requestedDate?: string;
effectiveTradeDate?: string;
updatedAt?: string;
source: string;
data: T | null;
message?: string;
}
这里最重要的不是类型本身,而是把“没有结果”拆开。
empty:请求成功,但这个日期确实没有记录;not_ready:交易日有效,但数据还没有更新完成;forbidden:当前账号没有权限;rate_limited:触发频率或额度限制;error:网络、字段变化或上游服务异常。
Agent 只有看到这些状态,才能决定是继续查询、切换工具、稍后重试,还是明确告诉用户当前无法确认。
请求日期和有效交易日必须分开
股票任务经常使用“今天”“最近一天”这样的自然语言。周末、节假日和盘后更新时间会让它们产生歧义。
例如用户在周六查询行情,工具可以返回周五的数据,但至少要同时保留:
{
"requestedDate": "2026-07-18",
"effectiveTradeDate": "2026-07-17",
"updatedAt": "2026-07-17T18:05:00+08:00"
}
这样 Agent 可以准确地说“最近一个交易日为 7 月 17 日”,而不会把周五收盘价描述为周六实时行情。
K 线工具还要额外说明频率和复权方式。日线、分钟线、实时快照不能共用一个模糊的 price 字段;前复权、不复权和后复权也不应该在一次分析中混用。
在适配器里统一代码、单位和字段
不同股票数据源常见的差异包括:
- 股票代码是
000001.SZ、sz.000001还是000001; - 成交额使用元、万元还是千元;
- 涨跌幅返回
2.35还是0.0235; - 时间是交易日、自然日还是带时区的时间戳;
- 空值是
null、NaN、空字符串还是缺少字段。
这些转换应在适配器中完成,不应交给提示词猜。
def normalize_quote(raw, source):
if raw is None or len(raw) == 0:
return {
"status": "empty",
"source": source,
"data": None,
"message": "查询成功,但没有匹配记录",
}
row = raw.iloc[0]
return {
"status": "ok",
"source": source,
"effectiveTradeDate": to_trade_date(row["trade_date"]),
"data": {
"symbol": normalize_symbol(row["symbol"]),
"close": float(row["close"]),
"changePct": normalize_percent(row["pct_chg"]),
"amountYuan": normalize_amount(row["amount"], source),
},
}
真正的生产代码还需要处理重试、超时、字段缺失和日志,但转换规则应该集中在这一层。以后替换数据源时,Agent 工具的返回结构可以保持不变。
工具要按任务拆,不要做“万能股票查询”
统一返回层并不意味着只提供一个接口。相反,给 Agent 的工具越应该边界清楚。
例如可以拆成:
- 市场概览:指数、成交额、上涨和下跌家数;
- 个股行情:实时快照或最近有效快照;
- 历史 K 线:频率、复权方式和日期范围明确;
- 涨跌停结构:涨停池、跌停池、炸板和连板梯队;
- 公告事件:发布日期、事件日期和来源可追溯;
- 龙虎榜:明确“未上榜”与“查询失败”的区别。
如果把这些能力全部塞进一个 query_stock,模型不仅要猜参数,还要猜返回结构。工具拆分后,无论使用函数调用还是 MCP,Agent 都更容易先选对工具,再组合结果。
上线前重点测六种边界情况
我会至少准备下面六组用例,而不是只测一次正常返回:
| 场景 | 预期结果 |
|---|---|
| 周末查询“今天行情” | 返回最近交易日,并同时保留请求日期 |
| 查询未上龙虎榜的股票 | empty,不能写成接口异常 |
| 交易日盘后数据尚未生成 | not_ready,允许稍后重试 |
| API Key 权限不足 | forbidden,不能伪装成空数据 |
| 请求过快或额度耗尽 | rate_limited,给出可重试信息 |
| 上游字段改名 | error 并记录原始异常,不能返回半套字段 |
这六类状态一旦混在一起,Agent 输出再流畅也不可靠。
自建适配层还是使用现成工具层
如果团队已经有 Tushare、AkShare、授权 API 或内部数据库,自己做适配层的好处是数据口径和历史资产都掌握在手里,适合长期维护和定制研究。
如果目标是先让 Agent 完成 A股市场概览、涨停梯队、题材资金、龙虎榜和事件查询,也可以评估现成的 MCP 工具层。例如悟道 A股股票数据 MCP 已经把这些能力拆成只读工具,适合用来验证 Agent 工作流。无论使用哪一种方案,仍然要核对返回日期、字段口径、权限和异常状态。
数据源决定能拿到什么,统一返回层决定 Agent 能不能正确理解。对股票 Agent 来说,后者往往才是从“可以演示”走向“可以重复运行”的关键。
本文只讨论数据工程和 Agent 工具设计,不构成投资建议。