给股票 Agent 设计统一数据返回层:日期、空值和错误状态怎么处理

5 阅读5分钟

最近在做股票 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.SZsz.000001 还是 000001
  • 成交额使用元、万元还是千元;
  • 涨跌幅返回 2.35 还是 0.0235
  • 时间是交易日、自然日还是带时区的时间戳;
  • 空值是 nullNaN、空字符串还是缺少字段。

这些转换应在适配器中完成,不应交给提示词猜。

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 工具设计,不构成投资建议。