主题
完整查询示例 — 一次 trace 拆解
用 v0.1.1 跑一次「查 600519 的当前价格 + 最近一季 ROE」,把每一步的输入、输出、cite 全展开。
所有数字、
tool_call_id、fetched_at都来自 2026-05-07 21:42 BJT 的真实运行(盘后路径)。把它当作 reference 文档而不是教程。
用户输入
查 600519 的当前价格和最近一季 ROEStep 1 — Skill 选择
Hermes LLM 根据 ~/.hermes/profiles/stock-research-agent/skills/research/stock-research/SKILL.md 里的触发条件("用户提到 6 位数字股票代码 + 财务问询")选择 stock-research skill。
LLM 决定调用两个工具:
fetch_price.py 600519fetch_fundamentals.py 600519 --period 20251231
Step 2 — fetch_price.py 600519
入口(skill/stock-research/scripts/fetch_price.py):
python
client = get_client() # DirectClient (无 TWILIGHT_SERVICE_URL)
envelope = client.get_price(normalize_code("600519")) # → "600519.SH"
emit(envelope)_client.DirectClient.get_price 转入 core/core/data/schemas.py:get_price:
python
def get_price(code: str, *, trade_date=None, token: str):
if trade_date is None and _market.is_market_open(token=token):
try:
return akshare.get_spot_price(code) # 路径 A: 实时
except AkshareError:
pass # 降级走 Tushare
# 路径 B: Tushare daily
items = tushare.call(api_name="daily", ...)
return _claim(..., source="tushare", served_by="tushare")_market.is_market_open 检查:
- 当前北京时间:
2026-05-07 21:42:31(Thursday) - 21:42 > 15:00 → 超出交易时段
- 直接返回
False,不调 Tushare trade_cal
→ 走路径 B(Tushare daily)。
Tushare 调用:
http
POST http://api.tushare.pro
{
"api_name": "daily",
"token": "<TUSHARE_TOKEN>",
"params": {"ts_code": "600519.SH"},
"fields": "ts_code,trade_date,close"
}返回:
json
{
"code": 0,
"msg": "",
"data": {
"fields": ["ts_code", "trade_date", "close"],
"items": [["600519.SH", "20260507", 1371.05]]
}
}stdout 输出(fetch_price.py 的最终 envelope):
json
{
"value": 1371.05,
"metric": "close",
"code": "600519.SH",
"as_of": "2026-05-07",
"cite": {
"kind": "tool",
"source": "tushare",
"table": "daily",
"fetched_at": "2026-05-07T13:42:31.168698+00:00",
"tool_call_id": "tc_fed71513e34b",
"served_by": "tushare"
}
}关键观察:
metric: "close"、served_by: "tushare"→ agent 必须说 "最近收盘",不能说 "当前价"as_of: "2026-05-07"是 Tushare 返回的最新行;如果是周末调用,会是上周五
Step 3 — fetch_fundamentals.py 600519 --period 20251231
走 core/core/data/schemas.py:get_fundamentals,调 Tushare fina_indicator:
http
POST http://api.tushare.pro
{
"api_name": "fina_indicator",
"params": {"ts_code": "600519.SH", "period": "20251231"},
"fields": "ts_code,end_date,roe,grossprofit_margin,netprofit_margin,debt_to_assets"
}stdout 输出(截短,仅展示 ROE 那一条 claim):
json
{
"code": "600519.SH",
"as_of": "2025-12-31",
"claims": [
{
"value": 36.21,
"metric": "ROE",
"code": "600519.SH",
"as_of": "2025-12-31",
"cite": {
"kind": "tool",
"source": "tushare",
"table": "fina_indicator",
"fetched_at": "2026-05-07T13:42:33.514220+00:00",
"tool_call_id": "tc_8a1a44b21fbb"
}
},
{ /* gross_margin, net_margin, debt_to_assets 同结构 */ }
]
}注意:get_fundamentals 暂未带 served_by(Tushare 是这条数据的唯一源,没有路由分支需要表达)。P1.2 多源后会补上。
Step 4 — Verifier 把关
core/core/verifier.py:Verifier.verify(claims, trace, registry) 做以下检查(per claim):
cite.kind == "tool"→ 在trace里找cite.tool_call_id,找不到 = fail- 找到的 ToolCall 记录的
value必须与 claim 的value等值(浮点 abs_tol=1e-9) cite.source与 ToolCall.source 一致as_of距fetched_at不超过 staleness budget(默认 365×10 天)cite.kind == "competence"→competence_id必须在CompetenceRegistry里注册
任何一项失败 → VerificationResult(ok=False, failures=[...]):
python
class VerificationFailure:
claim_index: int
reason: str # "tool_call_id 'tc_xxx' missing from trace"SingleAgentRuntime.run() 看到 ok=False → 把 last_failures 拼到 user prompt 里,重试一次。两次都失败 → 返回 VerifiedFailure 给 Hermes,Hermes 把错误告诉用户(不静默降级)。
Step 5 — Agent 转写为自然语言
LLM 拿到两个 verified envelope,依据 SKILL.md templates/operation-strategy.md 模板组装输出:
markdown
## 600519 (贵州茅台) 速览
- **最近收盘**:¥1371.05(Tushare daily, 2026-05-07)
- **ROE (FY2025)**:36.21%(Tushare fina_indicator, 2025-12-31)
> 注:当前为盘后查询,价格为最新收盘价,非实时盘口。强制纪律(SKILL.md §Output):
- 每个数字必须能映射回某个 cite envelope(tool 或 competence)
- 不能"凭感觉" — 没有 cite 就不能写
metric: close用"最近收盘"措辞,metric: current_price用"当前价"
如果 Verifier 失败会怎样?
构造一个失败场景:LLM 编造了一个数字 value: 1500 但 cite.tool_call_id 来自一个真实的 trace 调用(值是 1371.05)。
Verifier 一次扫描:
python
VerificationResult(
ok=False,
failures=[VerificationFailure(
claim_index=0,
reason="value mismatch for tc_fed71513e34b: claim=1500.0, trace=1371.05"
)]
)SingleAgentRuntime 把这个反馈塞回下一轮 prompt:
Your previous answer was rejected. Reason: value mismatch for
tc_fed71513e34b: claim=1500.0, trace=1371.05. Re-run the relevant
tool and use the actual returned value, or remove the claim.LLM 重试。仍失败 → user 看到的不是 1500,而是错误说明 + 建议("请稍后重试或提供更具体的时间范围")。
为什么这套设计值得
| 没有这套机制会怎样 | 我们的做法 |
|---|---|
| LLM 说"P/E 是 25",没人知道是从哪查的、什么时候查的 | 每个数字必须有 tool_call_id 指向真实的 trace 记录 |
| 老的研报里说 28,新数据是 25,LLM 混着用 | as_of 强制 + staleness budget |
| 一个 LLM 让另一个 LLM "校验",幻觉互相确认 | Verifier 是确定性代码,没有任何 LLM 参与 |
| 数据源换了,所有调用方都要改 | core 数据层抽象 + cite 信封 schema 稳定 |
| 盘中查询拿到隔夜收盘还说"当前价" | served_by + metric 双重区分(v0.1.1 hotfix) |
进一步阅读
- 这一切的工程实现:
core/core/{citation,verifier,competences,runtime,tools}.py - 数据流的高层视角:数据流转
- 协议设计动机:ADR-0002 Citation 协议