一个轻量、可替换模型的事实驱动型上下文时间序列预测 Agent。不训练模型,也不让 LLM 直接
生成预测值。整体架构为 ReAct:历史阶段由一个 greedy 单步 Historical ReAct
Controller 驱动——每轮观察当前 working history H_t、重建后的数值证据与已接受的
action trajectory,只选择一个最必要动作并立即在新 H_t 上生效;PREDICT 结束历史
阶段并声明预测后可控制的未来窗口。随后 TSFM 生成九条分位数轨迹(q0.1…q0.9),
Resolver 只在这些窗口内选择分位等级。
设计原则:
MASK、USE_CURRENT、UNDO
修改或恢复当前 H_t;PREDICT 是阶段边界:提交 H_t,同时携带 deferred
future_control_windows;未来控制不再是历史 action;当前已准备好可选的 DeepSeek API、本地 Qwen3.5 与 Toto 2.0 适配器,但都不会默认调用。
GiftCtxRulePlanner 和 LastValueTSFMStub 只用于开发、测试接口和跑通数据,
不能作为正式模型或基准结果。
评测使用 GIFT-CTX。
将数据集放到本地目录:
git clone https://huggingface.co/datasets/Salesforce/GIFT-CTX
git clone https://github.com/Crash0524/Forecast-Agent.git
cd Forecast-Agent
项目依赖 Python 3.10+、NumPy、pandas;读取 GIFT-CTX Parquet 还需要 PyArrow。 在主环境安装(含数据 / DeepSeek / 开发依赖):
pip install -r requirements.txt
⚠️ 重点:Toto 2.0 运行在与主环境分离的 Conda 环境里。 模型通过通用 JSON 配置 +
模型注册表加载(src/forecast_agent/adapters/models/registry.py),不会往 CLI 加
模型专属参数。Toto2 有两种后端:
| 后端 | 配置 mode | 说明 |
|---|---|---|
| worker(默认) | "worker" | 在独立 Conda 环境(默认名 agent_toto)中启动持久 worker 进程;主进程不需要安装 PyTorch/Toto,适合与外部 LLM API 组合 |
| in_process | "in_process" | 当前环境已安装 torch + toto2 时懒加载,进程内直接推理 |
config/models/toto2-2.5b.json 关键字段:
{
"mode": "worker", // worker 或 in_process
"conda_env": "agent_toto", // worker 模式下,模型运行在哪个 Conda 环境
"model_path": "/home/menchunting/work/model-cache/Datadog--Toto-2.0-2.5B",
"device": "cuda:0", // GPU 编号
"local_files_only": true, // 仅使用本地权重缓存
"startup_timeout": 900.0,
"forecast_timeout": 300.0
}
因此运行链路是:轻量主环境(LLM/编排) + agent_toto Conda 环境(Toto 推理)
两套环境。conda_env 必须指向已安装 torch 与 toto2 包、并能访问
model_path 权重的环境;worker 由主进程自动拉起,模型只加载一次,后续请求复用
同一进程。
LLM 通过 config/LLM/*.json 配置。可直接用 DeepSeek API
(deepseek-v4-flash.json / deepseek-v4-pro.json),API Key 从环境变量读取
(不提交真实 Key):
export DEEPSEEK_API_KEY="sk-..." # 方式一:环境变量
# 或方式二:通过 --env-file 读取 .env.example
也可用本地部署的 Qwen3.5(qwen3.5-4b-local.json / qwen3.5-9b-local.json):先启动
vLLM 服务(scripts/serve_qwen35_vllm.sh),再把 base_url 指向
http://127.0.0.1:8000/v1、model 指向本地权重路径即可。
批量 A/B/C 评估(245 条全量,前处理 + Toto + 后处理):
PYTHONPATH=src python scripts/evaluate_gift_ctx_preprocess_postprocess.py \
--data {data_dir} \
--model-config config/models/toto2-2.5b.json \
--llm-config config/LLM/qwen3.5-9b-local.json \
--llm-thinking disabled \
--env-file .env.example \
--output output/gift_ctx_v8.5_qwen9b_thinking.jsonl \
--raw-baseline
常用参数:--idx(单条)、--limit(条数上限)、--shard-count/--shard-index
(GPU 分片)、--resume(续跑跳过已成功样本)、--raw-baseline(额外跑一次无 LLM
的 raw,输出 A/B/C 三阶段归因,共两次 Toto 调用)。
raw facts + 权威时间轴 + working history H0
│
▼
┌──────────────────────────────────────────────────┐
│ Historical ReAct Controller(LLM) │
│ │
│ 观察:H_t + 重建的数值证据 + action trajectory │
│ 思考并选择唯一 action │
│ │
│ MASK / USE_CURRENT / UNDO │
│ → 修改/恢复 H_t → 重建证据 → 再观察 │
│ │
│ PREDICT + future_control_windows │
│ → 退出历史循环 │
└──────────────────────────────────────────────────┘
│
▼
TSFM:一次生成 q0.1 … q0.9
│
▼
future_control_windows 是否为空?
├── 是:本地全 horizon 选 q0.5(跳过 Resolver)
└── 否:Quantile Route Resolver(LLM)→ 本地校验并 gather Toto 数值
| 操作 | LLM 决策 | wire 字段 | 程序执行边界 |
|---|---|---|---|
MASK | 哪些有界历史窗口不应作为预测证据(reason_type 只标记区间性质) | start / end / reason_type | grounded 范围、保留足够有效点;写为 NaN,不插值 |
USE_CURRENT | 当前 regime 从哪个边界开始 | boundary | 检查边界与段长度;确定性截取 H_t |
UNDO | 最近一次历史编辑是否应撤销 | 无 | pop working-state stack;trajectory 仍保留完整审计 |
PREDICT | H_t 是否可提交;未来哪些窗口允许 resolver 处理 | future_control_windows | exactly-once TSFM;仅此 action 可携带 future_control_windows |
运行时 wire 使用拆分字段:MASK 和 future windows 使用 start/end,
USE_CURRENT 只使用包含式 boundary,Resolver overrides 也使用 start/end。
程序内部统一按左闭右开 [start, end) 执行;当 end 恰好是当前历史或预测期的最后
timestamp 时,是否包含最后点继续遵循对应的 final-point policy。旧静态 plan 的
start/end 双闭输入仍由 compatibility parser 转换为等价执行范围;旧静态
normalized history context 也只在 compatibility executor 中保留,不属于
Historical Controller action space。
历史 Controller 默认最多执行 8 轮,可在 LLM 配置中通过
max_history_steps 设置,或在评估命令中用 --max-history-steps N 覆盖(1–32)。
步数上限只由外层程序执行并写入结果元数据,不发送给 LLM;如果在上限内没有得到
PREDICT,程序会在调用 TSFM 前安全终止。
Historical Controller 的每轮输入包含 text_context、完整当前 current_history、
current_conditioning、history_summary、real_timeline、压缩后的
action_history 和 last_observation。history_summary 保留文本时间/区间与处理后
数值证据的紧凑绑定,便于 LLM 联合判断事实与 working history。Action history 只向
LLM 暴露 action、arguments、reason,完整执行结果仍保留在最终 trajectory 中。
Prompt 版本常量仅用于评估元数据与 resume 兼容校验,不写入任何实际发送给 LLM 的
system/user message。
评估 JSONL 的 llm_responses.historical_controller_rounds 按 Controller 轮次
输出原始 LLM response。每轮包含对应的 working-history 起点和长度,以及该轮所有
retry response、校验结果与原始 JSON 内容。
Historical Controller 的每个 step 与 Quantile Resolver 阶段都最多发送三次请求 (初次请求加两次 retry)。retry prompt 只携带当前阶段最近一次失败的 response、 错误类型、具体原因及当前请求轮次;当前 step 成功后,下一 step 不继承此前的 retry 对话,只保留已经接受的 action trajectory 和重新构建的 working state。
src/forecast_agent/
agent.py # 编排:ForecastAgent 与 PreprocessPostprocessForecastAgent
interfaces.py # LLM / TSFM / Preprocessor / Postprocessor / 边界检测接口
models.py # action、plan、window、result 等数据模型
prompting.py # 单阶段 ContextPlan prompt、JSON Schema、语义校验
prompting_preprocess_postprocess.py # ReAct controller 与 quantile resolver prompt
validators_preprocess_postprocess.py # schema/parser 与 quantile routing 校验
evidence.py # 时间归属、历史局部证据和数值候选
history.py # missing mask、历史编辑执行与归一化
future.py # 未来确定性编辑
boundary.py # 确定性阶段边界检测
intervals.py # 时间区间解析与执行范围
timeline.py # history / future 时间轴构建
metrics.py # MAE 等指标
cli.py # 单样本 direct / llm 命令行入口
adapters/
deepseek.py # DeepSeek/OpenAI 兼容 LLM 适配器
deepseek_preprocess_postprocess.py # ReAct preprocessor 与 quantile postprocessor
gift_ctx_rules.py # 开发用规则 planner(不可作为基准)
stubs.py # 开发桩
models/ # TSFM registry、Toto2 与 worker 后端
data/ # GIFT-CTX 读取
config/
models/ # 模型运行配置(Toto2 等)
LLM/ # LLM 配置(deepseek-v4 / qwen3.5 本地)
scripts/ # 批量评估、分片合并、vLLM 启动
tests/ # 单元与端到端测试
doc/ # 架构、数据映射、模型接入与验收报告
| 指标 | A (raw) | B (前处理 q0.5) | C (最终) |
|---|---|---|---|
| mean MAE | 553.90 | 270.48 | 269.12 |
| median MAE | 8.08 | 8.22 | 8.08 |
| 阶段 | 提升 | 不变 | 降低 | 提升和 | 损失和 | 净改善 |
|---|---|---|---|---|---|---|
| 前处理 B vs A | 58 | 130 | 56 | 76,603.1 | 7,449.9 | +69,153.3 |
| 路由 C vs B | 55 | 188 | 1 | 333.6 | 0.0 | +333.6 |
| 最终 C vs A | 111 | 78 | 55 | 76,936.7 | 7,449.9 | +69,486.8 |
| 分组 | pre (w/t/b) | pre 净 | route (w/t/b) | route 净 | final (w/t/b) | final 净 |
|---|---|---|---|---|---|---|
| anomaly (60) | 22/12/26 | −4,526.5 | 0/60/0 | +0.0 | 22/12/26 | −4,526.5 |
| phase_change (60) | 23/19/18 | +75,810.0 | 1/59/0 | +0.3 | 24/19/17 | +75,810.3 |
| future (124) | 13/99/12 | −2,130.1 | 54/69/1 | +333.3 | 65/47/12 | −1,796.9 |
| CIK (120) | 18/82/20 | −2,311.5 | 39/80/1 | +158.0 | 55/46/19 | −2,153.5 |
| GIFT synthesize (124) | 40/48/36 | +71,464.8 | 16/108/0 | +175.6 | 56/32/36 | +71,640.4 |
7 commits
Python
99.7%
一个轻量、可替换模型的事实驱动型上下文时间序列预测 Agent。不训练模型,也不让 LLM 直接
生成预测值。整体架构为 ReAct:历史阶段由一个 greedy 单步 Historical ReAct
Controller 驱动——每轮观察当前 working history H_t、重建后的数值证据与已接受的
action trajectory,只选择一个最必要动作并立即在新 H_t 上生效;PREDICT 结束历史
阶段并声明预测后可控制的未来窗口。随后 TSFM 生成九条分位数轨迹(q0.1…q0.9),
Resolver 只在这些窗口内选择分位等级。
设计原则:
MASK、USE_CURRENT、UNDO
修改或恢复当前 H_t;PREDICT 是阶段边界:提交 H_t,同时携带 deferred
future_control_windows;未来控制不再是历史 action;当前已准备好可选的 DeepSeek API、本地 Qwen3.5 与 Toto 2.0 适配器,但都不会默认调用。
GiftCtxRulePlanner 和 LastValueTSFMStub 只用于开发、测试接口和跑通数据,
不能作为正式模型或基准结果。
评测使用 GIFT-CTX。
将数据集放到本地目录:
git clone https://huggingface.co/datasets/Salesforce/GIFT-CTX
git clone https://github.com/Crash0524/Forecast-Agent.git
cd Forecast-Agent
项目依赖 Python 3.10+、NumPy、pandas;读取 GIFT-CTX Parquet 还需要 PyArrow。 在主环境安装(含数据 / DeepSeek / 开发依赖):
pip install -r requirements.txt
⚠️ 重点:Toto 2.0 运行在与主环境分离的 Conda 环境里。 模型通过通用 JSON 配置 +
模型注册表加载(src/forecast_agent/adapters/models/registry.py),不会往 CLI 加
模型专属参数。Toto2 有两种后端:
| 后端 | 配置 mode | 说明 |
|---|---|---|
| worker(默认) | "worker" | 在独立 Conda 环境(默认名 agent_toto)中启动持久 worker 进程;主进程不需要安装 PyTorch/Toto,适合与外部 LLM API 组合 |
| in_process | "in_process" | 当前环境已安装 torch + toto2 时懒加载,进程内直接推理 |
config/models/toto2-2.5b.json 关键字段:
{
"mode": "worker", // worker 或 in_process
"conda_env": "agent_toto", // worker 模式下,模型运行在哪个 Conda 环境
"model_path": "/home/menchunting/work/model-cache/Datadog--Toto-2.0-2.5B",
"device": "cuda:0", // GPU 编号
"local_files_only": true, // 仅使用本地权重缓存
"startup_timeout": 900.0,
"forecast_timeout": 300.0
}
因此运行链路是:轻量主环境(LLM/编排) + agent_toto Conda 环境(Toto 推理)
两套环境。conda_env 必须指向已安装 torch 与 toto2 包、并能访问
model_path 权重的环境;worker 由主进程自动拉起,模型只加载一次,后续请求复用
同一进程。
LLM 通过 config/LLM/*.json 配置。可直接用 DeepSeek API
(deepseek-v4-flash.json / deepseek-v4-pro.json),API Key 从环境变量读取
(不提交真实 Key):
export DEEPSEEK_API_KEY="sk-..." # 方式一:环境变量
# 或方式二:通过 --env-file 读取 .env.example
也可用本地部署的 Qwen3.5(qwen3.5-4b-local.json / qwen3.5-9b-local.json):先启动
vLLM 服务(scripts/serve_qwen35_vllm.sh),再把 base_url 指向
http://127.0.0.1:8000/v1、model 指向本地权重路径即可。
批量 A/B/C 评估(245 条全量,前处理 + Toto + 后处理):
PYTHONPATH=src python scripts/evaluate_gift_ctx_preprocess_postprocess.py \
--data {data_dir} \
--model-config config/models/toto2-2.5b.json \
--llm-config config/LLM/qwen3.5-9b-local.json \
--llm-thinking disabled \
--env-file .env.example \
--output output/gift_ctx_v8.5_qwen9b_thinking.jsonl \
--raw-baseline
常用参数:--idx(单条)、--limit(条数上限)、--shard-count/--shard-index
(GPU 分片)、--resume(续跑跳过已成功样本)、--raw-baseline(额外跑一次无 LLM
的 raw,输出 A/B/C 三阶段归因,共两次 Toto 调用)。
raw facts + 权威时间轴 + working history H0
│
▼
┌──────────────────────────────────────────────────┐
│ Historical ReAct Controller(LLM) │
│ │
│ 观察:H_t + 重建的数值证据 + action trajectory │
│ 思考并选择唯一 action │
│ │
│ MASK / USE_CURRENT / UNDO │
│ → 修改/恢复 H_t → 重建证据 → 再观察 │
│ │
│ PREDICT + future_control_windows │
│ → 退出历史循环 │
└──────────────────────────────────────────────────┘
│
▼
TSFM:一次生成 q0.1 … q0.9
│
▼
future_control_windows 是否为空?
├── 是:本地全 horizon 选 q0.5(跳过 Resolver)
└── 否:Quantile Route Resolver(LLM)→ 本地校验并 gather Toto 数值
| 操作 | LLM 决策 | wire 字段 | 程序执行边界 |
|---|---|---|---|
MASK | 哪些有界历史窗口不应作为预测证据(reason_type 只标记区间性质) | start / end / reason_type | grounded 范围、保留足够有效点;写为 NaN,不插值 |
USE_CURRENT | 当前 regime 从哪个边界开始 | boundary | 检查边界与段长度;确定性截取 H_t |
UNDO | 最近一次历史编辑是否应撤销 | 无 | pop working-state stack;trajectory 仍保留完整审计 |
PREDICT | H_t 是否可提交;未来哪些窗口允许 resolver 处理 | future_control_windows | exactly-once TSFM;仅此 action 可携带 future_control_windows |
运行时 wire 使用拆分字段:MASK 和 future windows 使用 start/end,
USE_CURRENT 只使用包含式 boundary,Resolver overrides 也使用 start/end。
程序内部统一按左闭右开 [start, end) 执行;当 end 恰好是当前历史或预测期的最后
timestamp 时,是否包含最后点继续遵循对应的 final-point policy。旧静态 plan 的
start/end 双闭输入仍由 compatibility parser 转换为等价执行范围;旧静态
normalized history context 也只在 compatibility executor 中保留,不属于
Historical Controller action space。
历史 Controller 默认最多执行 8 轮,可在 LLM 配置中通过
max_history_steps 设置,或在评估命令中用 --max-history-steps N 覆盖(1–32)。
步数上限只由外层程序执行并写入结果元数据,不发送给 LLM;如果在上限内没有得到
PREDICT,程序会在调用 TSFM 前安全终止。
Historical Controller 的每轮输入包含 text_context、完整当前 current_history、
current_conditioning、history_summary、real_timeline、压缩后的
action_history 和 last_observation。history_summary 保留文本时间/区间与处理后
数值证据的紧凑绑定,便于 LLM 联合判断事实与 working history。Action history 只向
LLM 暴露 action、arguments、reason,完整执行结果仍保留在最终 trajectory 中。
Prompt 版本常量仅用于评估元数据与 resume 兼容校验,不写入任何实际发送给 LLM 的
system/user message。
评估 JSONL 的 llm_responses.historical_controller_rounds 按 Controller 轮次
输出原始 LLM response。每轮包含对应的 working-history 起点和长度,以及该轮所有
retry response、校验结果与原始 JSON 内容。
Historical Controller 的每个 step 与 Quantile Resolver 阶段都最多发送三次请求 (初次请求加两次 retry)。retry prompt 只携带当前阶段最近一次失败的 response、 错误类型、具体原因及当前请求轮次;当前 step 成功后,下一 step 不继承此前的 retry 对话,只保留已经接受的 action trajectory 和重新构建的 working state。
src/forecast_agent/
agent.py # 编排:ForecastAgent 与 PreprocessPostprocessForecastAgent
interfaces.py # LLM / TSFM / Preprocessor / Postprocessor / 边界检测接口
models.py # action、plan、window、result 等数据模型
prompting.py # 单阶段 ContextPlan prompt、JSON Schema、语义校验
prompting_preprocess_postprocess.py # ReAct controller 与 quantile resolver prompt
validators_preprocess_postprocess.py # schema/parser 与 quantile routing 校验
evidence.py # 时间归属、历史局部证据和数值候选
history.py # missing mask、历史编辑执行与归一化
future.py # 未来确定性编辑
boundary.py # 确定性阶段边界检测
intervals.py # 时间区间解析与执行范围
timeline.py # history / future 时间轴构建
metrics.py # MAE 等指标
cli.py # 单样本 direct / llm 命令行入口
adapters/
deepseek.py # DeepSeek/OpenAI 兼容 LLM 适配器
deepseek_preprocess_postprocess.py # ReAct preprocessor 与 quantile postprocessor
gift_ctx_rules.py # 开发用规则 planner(不可作为基准)
stubs.py # 开发桩
models/ # TSFM registry、Toto2 与 worker 后端
data/ # GIFT-CTX 读取
config/
models/ # 模型运行配置(Toto2 等)
LLM/ # LLM 配置(deepseek-v4 / qwen3.5 本地)
scripts/ # 批量评估、分片合并、vLLM 启动
tests/ # 单元与端到端测试
doc/ # 架构、数据映射、模型接入与验收报告
| 指标 | A (raw) | B (前处理 q0.5) | C (最终) |
|---|---|---|---|
| mean MAE | 553.90 | 270.48 | 269.12 |
| median MAE | 8.08 | 8.22 | 8.08 |
| 阶段 | 提升 | 不变 | 降低 | 提升和 | 损失和 | 净改善 |
|---|---|---|---|---|---|---|
| 前处理 B vs A | 58 | 130 | 56 | 76,603.1 | 7,449.9 | +69,153.3 |
| 路由 C vs B | 55 | 188 | 1 | 333.6 | 0.0 | +333.6 |
| 最终 C vs A | 111 | 78 | 55 | 76,936.7 | 7,449.9 | +69,486.8 |
| 分组 | pre (w/t/b) | pre 净 | route (w/t/b) | route 净 | final (w/t/b) | final 净 |
|---|---|---|---|---|---|---|
| anomaly (60) | 22/12/26 | −4,526.5 | 0/60/0 | +0.0 | 22/12/26 | −4,526.5 |
| phase_change (60) | 23/19/18 | +75,810.0 | 1/59/0 | +0.3 | 24/19/17 | +75,810.3 |
| future (124) | 13/99/12 | −2,130.1 | 54/69/1 | +333.3 | 65/47/12 | −1,796.9 |
| CIK (120) | 18/82/20 | −2,311.5 | 39/80/1 | +158.0 | 55/46/19 | −2,153.5 |
| GIFT synthesize (124) | 40/48/36 | +71,464.8 | 16/108/0 | +175.6 | 56/32/36 | +71,640.4 |
7 commits
Python
99.7%