让 LLM 自己决定"先查什么、再查什么"——多步取证之后,产出一份每条结论都挂证据的持仓诊断报告
注:本课接续第 3 课,把"单次打分器"升级成"会取证的 Agent"。
一个能跑的持仓诊断 Agent:你扔给它一句话——"我持有茅台,提价后是去是留?"——它自己规划取证路径:先调 get_news 看消息面,发现"批价回落"的分歧信号后,再调 get_price_history 用价格走势交叉验证,最后调 save_report 落笔成文,产出 data/diagnosis_report.md。报告里每条结论都标注证据来源。
这个"LLM 决定调哪个工具 → 执行 → 把结果喂回去 → 再决策"的循环,就是 ReAct(Reasoning + Acting)模式,也是 TradingAgents 论文里每个分析师角色的工作方式。第 3 课的打分器是"一次性组件",这节课的循环才是"Agent"。
第 3 课的模式是:你把新闻塞给 LLM,它打分,结束。对"持仓诊断"这种真问题,单次 Prompt 有三个硬伤:
| 单次 Prompt | Agent 工具调用循环 |
|---|---|
| 证据由你手工收集、一次性塞入,漏了就没了 | LLM 发现证据不足会自己再去取——看到"批价回落"才去查价格验证 |
| 结论基于你给的文本,无法核实出处 | 每个结论对应一次真实的工具调用,有据可查、可重放 |
| 换一个问题就要重写一遍流程 | 工具是稳定的地基,换个持仓问题只换一句任务描述 |
一句话:单次 Prompt 是"问答题",Agent 是"调研员"。对真金白银的持仓决策,你要的是后者。
打开 scripts/sentiment_agent.py,整个 Agent 只有三个工具,这是刻意的:
| 工具 | 职责 | 返回 |
|---|---|---|
get_news(symbol) | 拉个股新闻,东财接口失败退回内置样本 | dict:来源 + 条数 + 结构化新闻列表 |
get_price_history(symbol, start, end) | 读行情摘要,优先第 2 课存的本地 CSV | dict:区间涨跌幅、最高最低、近 5 日收盘 |
save_report(text) | 把最终报告写入 data/diagnosis_report.md | dict:保存路径 + 字数 |
pct_change、source),模型不会误读;程序也能对结果做二次校验。get_news 接口失败退回样本,get_price_history 发现本地 CSV 不覆盖请求区间也退回样本——并且都在 source 字段里如实标注数据出处。这是第 2 课"免费接口一定会挂"纪律的延续:降级不丢人,隐瞒数据来源才丢人。真实模式里,模型选择工具时读的是 tools schema 里的 description——它写得好坏直接决定模型会不会用错。"拉取最新新闻,用于收集消息面证据"比"获取数据"强十倍。写 description 时想象你在给一个聪明但完全不了解你代码的同事写交接文档。
脚本里 run_live() 的循环骨架只有几步,值得逐行读懂(这是展示用代码,不必复制运行):
for step in range(1, MAX_STEPS + 1):
msg = chat_with_tools(messages, api_key)["choices"][0]["message"]
messages.append(msg)
if not msg.get("tool_calls"): # 模型不再要工具 = 它认为可以收尾
break
for tc in msg["tool_calls"]: # 模型这一步可能要调多个工具
result = run_tool(tc["function"]["name"], json.loads(tc["function"]["arguments"]))
messages.append({"role": "tool", "tool_call_id": tc["id"],
"content": json.dumps(result, ensure_ascii=False)})
关键点只有一个:工具的执行结果会以 role: "tool" 的消息追加回对话历史,下一轮请求时模型连同自己的"思考"和"观察"一起看到,于是它能基于新证据改变主意——第 2 步去查价格,正是因为第 1 步的新闻里出现了"批价回落"。这个"观察改变计划"的能力,单次 Prompt 给不了。
另一个工程细节:MAX_STEPS = 8 是循环保险丝。模型偶尔会陷入"调工具 → 不满意 → 再调同一个工具"的空转,没有上限就是烧钱死循环。任何 Agent 循环都必须有。
演示模式里,LLM 的"决策"是预计算的轨迹,但三个工具全部真实执行——你看到的每个数字都是工具现场算出来的:
cd "$HOME/AI Project/Quant Learning"
source .venv/bin/activate
python scripts/sentiment_agent.py
你会看到(我已在你的环境实测通过):
────────── 第 1 步 ──────────
思考:先拿新闻面证据——提价事件后,公司公告与媒体分别怎么表述
调用:get_news(symbol="600519")
观察:6 条新闻(来源:内置样本(演示模式不访问网络))
· 飞天茅台 官宣涨价!+100元
· 贵州茅台:上调飞天茅台i茅台平台零售价及合同价
· 茅台调价后今日股价盘中大涨5%,白酒股集体跟涨
· ……共 6 条
────────── 第 2 步 ──────────
思考:新闻里出现「批价回落」的空方信号,必须用价格走势交叉验证
调用:get_price_history(symbol="600519", start="2026-07-01", end="2026-07-21")
观察:16 个交易日(来源:内置样本(本地CSV仅覆盖 2024-01-02 ~ 2024-12-31,不含 2026-07-01 ~ 2026-07-21))
1612.50 → 1771.20(+9.84%)|最高 1784.79(2026-07-20)
────────── 第 3 步 ──────────
思考:新闻(利好)/ 批价(隐忧)/ 走势(验证)三方证据齐全,落笔成文
调用:save_report(text=<报告正文,874 字>)
观察:已写入 data/diagnosis_report.md(874 字)
注意第 2 步的观察行:get_price_history 先读了本地 CSV,发现它只覆盖 2024 年、不含事件区间,于是退回内置样本——并且把这个事实写进了 source 字段。数据覆盖范围检查以后会成为你的肌肉记忆:拿 2024 年的数据去论证 2026 年的事件,是回测之外的另一种"数据造假"。
打开生成的 data/diagnosis_report.md(以下为真实生成内容的节选):
### 1. 新闻面:提价落地,直接利好 【来源:get_news】
- 公司公告:飞天茅台零售价上调 100 元至 1639 元/瓶,7 月 18 日起执行
(《飞天茅台 官宣涨价!+100元》)。
### 2. 渠道面:批价回落,与提价背离 【来源:get_news】
- 官方提价的同时,渠道批价(经销商之间的真实成交价)在回落。
- 含义:终端需求偏弱时提价,可能挤压渠道利润——本案最大的空方证据。
### 3. 价格走势:市场短期投票支持提价 【来源:get_price_history】
- 2026-07-01 ~ 2026-07-21 共 16 个交易日:1612.50 → 1771.20(+9.84%)。
- 提价公告日(2026-07-17)收盘 1658.40,此后 +6.80%。
这份报告的骨架就是"三方证据对质":新闻面利好(提价公告)vs 渠道面隐忧(批价回落)vs 价格面验证(区间 +9.84%)。两多一空,结论是"继续持有、暂不加仓、盯批价"——而不是拍脑袋的"强烈看涨"。每个【来源】标注让你可以倒查任何一条结论:不信,就去翻那一步的工具输出。这就是"结论有据可查"的落地形态。
报告结尾还自带两条纪律:T+1 提醒(盘后分析是"为明天写计划",最早明日才能执行)和数据局限声明(样本数据如实标注)。
演示模式的决策轨迹是我预计算的;真实模式把"决定调哪个工具"交还给模型。协议就是 OpenAI 兼容的 function calling:请求里带 tools schema(脚本里的 TOOLS 列表),模型回复里带 tool_calls,程序执行后把结果以 role: "tool" 追加回对话,循环直到模型调用 save_report 收尾或给出最终文本。
export LLM_API_KEY="sk-你的key"
python scripts/sentiment_agent.py --live
换别家模型只需改 LLM_BASE_URL 和 LLM_MODEL 两个环境变量。真实模式我已写好但尚未实测(你机器上还没有 Key)——拿到 Key 后跑一次,有报错直接贴给我。真实模式下 get_news 会真实请求东财接口(带 15 秒超时与样本兜底,单次调用,不会循环猛刷)。
第 6 课 · 盘中异动监控 Agent:把今天的工具循环搬到盘中——定时轮询行情,放量异动时让 Agent 自动取证(新闻+走势)并推送提醒,搭起"信号 → 人工确认"的半自动闭环。
有任何不清楚的地方(比如 function calling 协议、工具 description 写法、保险丝设计),直接问我——我是你的老师。