主题
数据流转 — Agent 是怎么工作的
这页解释一次用户查询从 WeChat 输入到带 cite 的回答之间,数据在 5 层架构里怎么流。读完应该能回答:
- "为什么我的 600519 价格不是当前价?" — 见 价格新鲜度
- "agent 凭什么不能编数字?" — 见 Verifier
- "akshare 还是 Tushare 出的数?" — 见
served_by
整体序列
User Hermes Skill core/ External
(WeChat 等) gateway stock-research data layer data source
│ │ │ │ │
│ "查 600519 当前价格" │ │ │ │
├──────────────────────►│ │ │ │
│ │ route │ │ │
│ ├─────────────►│ │ │
│ │ │ fetch_price.py │ │
│ │ │ 600519 │ │
│ │ ├─────────────────►│ │
│ │ │ │ get_price(...) │
│ │ │ │ │
│ │ │ ├ is_market_open? ─►│ Tushare
│ │ │ │ │ trade_cal
│ │ │ │ ├ yes │
│ │ │ ├──────────────────► │ akshare
│ │ │ │ │ Xueqiu spot
│ │ │ │ ◄──────────────────┤
│ │ │ │ value + as_of │
│ │ │ │ │
│ │ │ ◄────────────────┤ │
│ │ │ cite envelope │ │
│ │ │ (served_by) │ │
│ │ │ │ │
│ │ ├ Verifier check ──┘ │
│ │ │ - tool_call_id 在 trace? │
│ │ │ - value 与 trace 一致? │
│ │ │ - cite 字段齐全? │
│ │ │ │
│ │ ◄────────────┤ │
│ │ verified │ │
│ │ JSON │ │
│ ◄─────────────────────┤ │ │
│ "当前价 ¥1801.23 │ │ │
│ (akshare, │ │ │
│ 2026-05-07)" │ │ │每一步在干什么
1. User → Hermes gateway
用户在 WeChat 发消息。Hermes 的 platform adapter(channel_directory.json 里注册)把消息送进 gateway,gateway 进入 agent loop。
2. Hermes → Skill
LLM 根据 SKILL.md 的 trigger conditions 决定调哪个 skill。stock-research 的触发条件包括"用户提到股票代码"和"用户问 P/E、估值、技术面"。详见 skill/stock-research/SKILL.md §Trigger Conditions。
3. Skill → core data layer
skill scripts (fetch_price.py 等) 是无状态的 CLI 入口。它们通过 _client.py:get_client() 拿到一个客户端:
TWILIGHT_SERVICE_URL已设 →ServiceClient走 HTTP 后端(P1.0 backend,待发)- 否则 →
DirectClient直接 importcore.data.schemas
v0.1.0 现状:所有 profile 都跑 Direct 模式。P1.0 backend 上线后 user profile 切到 Service 模式,token 由 backend 持。
4. core 数据层路由
core/core/data/schemas.py:get_price 做自动路由:
| 条件 | 走哪 | metric | 延迟 |
|---|---|---|---|
trade_date 显式给值 | Tushare daily | close | T+1 |
trade_date 为 None + 市场开盘 | akshare Xueqiu | current_price | 实时(秒级) |
trade_date 为 None + 市场关闭 | Tushare daily | close | 最近交易日收盘 |
"市场是否开盘"由 core/core/data/_market.py:is_market_open() 判断,调 Tushare trade_cal 校验当日是否交易日(缓存当天结果)。失败时降级为"周一到周五 09:30–15:00 即开盘"。
5. cite 信封返回
每次成功查询返回一个固定形态的 envelope:
json
{
"value": 1801.23,
"metric": "current_price",
"code": "600519.SH",
"as_of": "2026-05-07",
"cite": {
"kind": "tool",
"source": "akshare",
"table": "stock_individual_spot_xq",
"fetched_at": "2026-05-07T05:30:12.456+00:00",
"tool_call_id": "tc_71387d9cc564",
"served_by": "akshare"
}
}下游消费者(Verifier、agent prose)只看 envelope,不重做网络调用。
6. Verifier 把关
core/core/verifier.py 是确定性代码(不是另一个 LLM)。每个声明必须满足:
cite.tool_call_id在 trace 里能找到value与 trace 里记录的值一致(浮点容差 1e-9)competence_id(如果是 knowledge cite)已注册as_of与 staleness budget 不冲突(默认 365 × 10 天,metric 级可覆盖)
校验失败 → 带 feedback 重试一次 → 仍失败则直接报错给 user,不静默降级。
7. Hermes → User
verified JSON 通过 LLM 转写成自然语言:「当前价 ¥1801.23(akshare,2026-05-07 实时)」。SKILL.md 要求 LLM 在叙述里区分 current_price(说"当前价")和 close(说"最近收盘")。
价格新鲜度
为什么会区分两条路径?
fetch_price.py 600519 在交易时段(09:30–15:00 BJT 工作日)调用 → Tushare daily API 设计上只返回已收盘交易日的收盘价(T+1 可用),所以 11:00 调 → 拿到昨天收盘。akshare 的 Xueqiu 单 code 端点(stock_individual_spot_xq)返回当前实时价,弥补了这个 gap。
这也是 v0.1.0 → v0.1.1 hotfix 的根本原因。详见 docs/planning/06-v0.1.1-hotfix-and-p1.md(草稿)。
served_by
cite 信封的 served_by 字段是 v0.1.1 引入的(向后兼容:老 cite 没这字段也能解析)。它告诉下游 "这次查询究竟是哪个 adapter 出的数",对两类场景关键:
- Verifier 跨源比对:P1.2 多源数据落地后,仓库会存 Tushare + pytdx3 + Yahoo 的同一日数据;如果三者差 > 0.01% 要预警。
served_by让审计直接知道这条 claim 选了谁。 - Agent 叙述风格:
served_by="akshare"→ 说"实时";served_by="tushare"不带trade_date→ 说"昨日收盘"。
进一步阅读
- 完整 cite 协议:Citation 协议
- 5 层架构总览:5 层架构
- 一次完整查询的 trace 示例:
example-trace - 多源数据规划:01 — 数据层