AGENTS.md 29 KB

AGENTS.md — deep_agent 2.0 工程纪律

本文件是给在本仓库里干活的人和 AI 助手看的硬约束。改代码前先读这里; 与这里冲突的改动,要么别做,要么先改这里并说明理由。


〇、项目定位

2.0 是通风智能体系统的新一代骨架,参照 1.5 的对外契约重建,但内部实现 不做移植。定位是「可运行的骨架」,不是「功能完备的产品」:

  • 对外契约完全兼容 1.5(POST /api/chat 的 multipart 入参与 SSE 帧格式), 这样 1.5 的前端不改一行就能连上。
  • 对内一切重写。1.5 积累的 40 个业务工具、22 个业务技能、MCP 工具池、 计费/调度/子智能体管理页都不在本仓库范围内。
  • 骨架的价值在于结构与纪律,不在于功能数量。

一、五条不变式(改动前必读)

1. thread_id ≡ session_id

由 api/chat_routes._init_session 强制。客户端传别的值一律 404。

理由:checkpointer 是进程级共享的,允许客户端指定任意 thread_id 就等于 允许读写别人的图状态。这条不变式在 2.0 是全局的——包括定时任务 (它为自己的每次执行建独立会话,而不是另起一个 thread_id 命名法)。

2. 单进程(workers=1)

三处进程内状态:SQLite checkpointer、并发闸计数、APScheduler。 多 worker 下它们各自独立,症状是「同一会话续跑读不到上文的图状态」、 「并发闸形同虚设」、「定时任务被每个 worker 各触发一次」。

main._check_single_process() 会检测并告警。要扩多进程,先把这三处 换成 Redis/MySQL,别只改启动参数。

3. 计费口径以 provider 的真实 usage 为准

api/context_meter.extract_usage 优先读 usage_metadata,读不到才回落本地 估算,并把 usage_source 标成 estimate。

不要改成本地估算优先。1.5 就是估算优先,为此专门开了 fix/context-usage-accounting 分支修口径;估算值参与决策的代价是 「限额拦不住、账单对不上」,而且没人能一眼看出哪些数字是估的。

4. 来源标注由工具侧产出,不靠模型自觉

工具用 tools.registry.sourced(data=..., sources=[...]) 返回结构化的来源; api/source_tracker 从 ToolMessage 里抽取并去重。

不要在提示词里要求模型「记得写引用」。模型漏写和编造这两种失败模式 无法在事后可靠区分;读工具返回值就不存在这两个问题。

5. 摘要压缩用框架的中间件,不自造

deepagents 的 SummarizationMiddleware 会把被淘汰的历史追加落盘到 /conversation_history/{thread_id}.md,并把路径嵌进摘要正文,agent 之后 能自己 read_file 找回来。

两个后果要记住:

  • 不要再传一个 SummarizationMiddleware 给 create_deep_agent。 create_agent 会校验中间件 name 唯一,重复直接抛 "Please remove duplicate middleware instances"(1.5 时代靠同名替换的 做法在 deepagents 0.6.x 已失效)。
  • 要关掉压缩,用 CTX_COMPACT_ENABLED=0(内部走 HarnessProfile.excluded_middleware)。关掉会同时失去历史落盘能力, 长会话会硬撑到超窗报错。

二、踩过的坑(别重犯)

这些都在开发过程中真实触发过,症状与原因距离很远,所以单独记下来。

坑 1:环境变量与 .env 的优先级

机器上存在全局 OPENAI_API_KEY(实测遇到过一把无效的)时,进程环境会盖掉 项目 .env。症状是「密钥明明配对了却报 401,且报错里显示的密钥后几位和 .env 里的不一样」。

config.py 把它做成显式可配的 DEEP_AGENT_ENV_PRIORITY:

  • dotenv(默认):.env 优先。防「被污染的 shell 静默改掉项目配置」
  • process:进程环境优先。跑变体实例必须用这档,例如 DEEP_AGENT_ENV_PRIORITY=process DB_DIALECT=mysql PORT=8081 python main.py
  • none:完全忽略 .env(测试/CI 用)

为什么默认不是 process:两种失败模式代价不对等。dotenv 的代价是「有意的 环境变量覆盖不生效」——加个前缀就绕过,且启动日志会告诉你;process 的代价是 「本机某个无关的全局变量静默改掉项目配置」——表现为身份诡异的 401 或连错库。

无论哪一档,冲突都会打 WARNING 并把键名暴露给 /health 的 config_conflicts,包括「生效值长什么样」(脱敏)。这条可见性是本机制的核心 价值:它把「配置没生效」从玄学变成一行日志。

注意 process 档会放行所有环境变量,包括那把无效的全局密钥——所以跑变体 实例时要连密钥一起显式传入,不要只传你要改的那几项。

坑 2:MemorySaver 之外,checkpointer 必须显式声明上下文窗口

摘要中间件的 trigger=("fraction", x) 依赖模型 profile["max_input_tokens"]。 不声明的话,框架在首次触发压缩时报错而不是降级。

agents/model_factory.build_model 无条件设置 profile={"max_input_tokens": window}。 新增任何模型构造路径都要照做。

坑 3:子智能体的 tools 必须是 BaseTool,主 agent 的 tools 可以是裸函数

两者要求不同:主 agent 的 tools= 由框架包装,子智能体的 tools 会被直接 塞进 ToolNode,那里要读 tool_.name。传裸函数会炸在 tool_.name 上, 而堆栈只指向 SubAgentMiddleware,看不出是参数形态问题。

用 tools.registry.get_tools_by_name([...]),它返回 BaseTool 列表。

坑 4:自定义中间件的同步/异步钩子都要实现

只实现 wrap_model_call 而服务全程走 astream 时,运行期抛 NotImplementedError,报错位置在模型节点内部。

agents/middleware.DynamicTimeMiddleware 同时实现了 wrap_model_call 与 awrap_model_call。

坑 5:ToolMessage 也走 messages 通道

stream_mode=["updates","messages"] 的 messages 通道里不只有 AIMessageChunk,ToolMessage 也在推。不过滤的话,工具返回的原始 JSON 会混进给用户看的正文。

对策:只在 isinstance(chunk, AIMessageChunk) 时发 token 事件。 同理,摘要中间件的模型调用也在同一通道,靠 metadata["lc_source"] == "summarization" 过滤。

坑 6:图输入必须是 dict

astream({"messages": [...]})。传裸 list 会被当成 __start__ 节点的返回值, 报 InvalidUpdateError: Expected dict, got [...]。

坑 7:load_builtin_tools 对已导入模块是空操作

工具的注册靠模块体里的装饰器,而 import_module 对 sys.modules 里 已有的模块不重放模块体。所以「清空注册表后重新装载」必须显式 reload, 否则注册表一直是空的,表现为「工具莫名其妙全没了」。

坑 8:时间戳精度决定消息顺序

created_at 只到秒时,同秒写入的 user/assistant 两条消息只能靠随机 UUID 做次级排序,顺序会随机颠倒——冷启动补种图状态时会把「谁说了什么」喂反。

db.db_conn.fmt_dt 输出到微秒,MySQL 侧对应 DATETIME(6)。

坑 9:两个计量器之间没有自动连接

ContextMeter(算上下文占用与用量)与 billing.meter.UsageMeter(入账) 是分开的。收尾时必须显式把前者的数字 add 给后者——漏了这一步, 交互式对话全程不会计费,而 /usage/today 显示 0,表面看「一切正常」。

坑 10:权限规则首条命中即返回

FilesystemPermission 是逐条匹配、命中即返回。所以具体的 allow 必须排在 宽泛的 deny 之前,否则 deny /** 会把后面的 allow 全部短路,agent 连技能文件都读不到。

坑 11:python main.py 会让模块被导入两次

uvicorn.run("main:app") 会再导入一次 main,于是模块级副作用执行两遍 (一次作为 __main__,一次作为 main)。日志会重复,配置会被构造两次。

副作用一律放进 lifespan,模块级只留纯定义。

坑 12:工具卡必须按 tool_call_id 索引,不能按工具名

同一轮里同一个工具会被调多次(实测 agent 会自己换关键词连查 4 次)。 只按 tool 名索引卡片 → 后一次的结果挂到前一张卡上,界面看起来「结果和 参数对不上」。

1.5 的契约里没有 tool_call_id,2.0 补了它作为附加字段(sse_events 的 tool_call / tool_result)。附加字段安全、改名不安全——见 §四的契约纪律。

坑 13:element.textContent += x 会摧毁子元素

textContent 的读是「所有后代文本的拼接」、写是「替换全部子节点」。所以 bubble.textContent += token 这类写法会:① 把子元素(工具卡片)的文本一起 读进来,② 赋值时把那些子元素整个删掉。表现是工具卡片被抹成一坨纯文本, 而且每次追加都重新拼接全文(O(N²))。

正确做法是给正文一个独立的文本节点,并在插入卡片时「断开」当前文本段, 以保留时间顺序(见 static/index.html 的 appendToken / addCardFor / breakSegment)。

坑 14:pdfminer 的阅读顺序是「按列」,表格会名称与数值错配

pdfminer.six 默认按内容流顺序输出文本,表格页抽出来是「先把一列名称全列完、 再列数值列」。把这段文本交给模型提取,上百行记录的名称与数值会张冠李戴,而且 文本看起来是通的,不核对就发现不了。

algorithms/airflow_review/parsers/plan_preprocess.py::_read_pdf 因此按坐标重建 视觉行序:按纵坐标聚类、簇内按横坐标排序,同一行多列用制表符分隔。判据是 「预处理产出的表格行合计能否对上文档声称的小计」——布尔台计划两套回风系统分别是 9228 与 17853,重建后能精确对上;tests/test_airflow_tools.py::test_real_plan_preprocess_is_faithful 把这条断言固化了下来。换 PDF 解析库时必须重跑它。

坑 15:待检项的容器不能预先去重

hierarchy._component_ids 曾对「组成对象列表」做 unique(),结果「重复汇总」 这个要检的缺陷永远检测不出来——去重把证据删掉了。同类陷阱:预处理里若按 「连续重复行即噪声」去重,会删掉表格第三列的类型标签(「硐室」「其他巷道」连续 出现),三元组断裂,表格识别结果直接归零。去重必须带长度与白名单门槛。

坑 16:参数口径决定公式变体,否则真实计划满屏假阳性

布尔台计划写 Q采1=60×11.3×1.0×1.2×1.5=1221:公式文字写了 70% 有效断面系数, 代入的 11.3 却已经是「平均有效净断面积」。复算若一律按 60×0.7×v×S×…,每个采面 都会被判成计算错误。

规则库为此支持 variants,按参数口径事实(section_area_basis)选公式。 新增公式前先看真实计划怎么代入,不要只看公式文字。同理,规则表里「按什么字段 匹配」与「区间边界字段叫什么」往往不同名(max_temperature vs min_temp), 所以表定义里要显式写 bounds。

口径标记还有一层坑:模型会把口径写成同义说法(实测把"有效断面积"写成"平均有效 净断面积"),精确匹配不命中就退回基础表达式,于是把一份正确计划算成"与计划不符"、 整盘阻断。因此有三道处置:① RuleSet.parameter_markers() 把允许取值(含 marker_values 声明的完整词汇表)下发给提取模板,从源头约束写法;② 命中不到时用 计划自己写出的结果反推变体(_infer_variant),并标 variant_selection = inferred_from_stated_calculation 提示人工核实;③ 自洽核验改为「在规则库允许的任一 口径下能复现计划写出的结果即算自洽」(_reproduce_with_candidates),一个都复现不 出来才是真算错。注意这里的候选集只来自规则库,不是任意表达式——所以"逐个试算"并没有 放宽判定。

坑 17:工具拿不到会话身份,但要读写会话工作区

工具是纯函数,tools/ 又不许依赖 agents/,可它必须把上传文件的虚拟路径 (/workspace/uploads/...)映射回真实沙箱路径。做法是用 langgraph.config.get_config() 取运行层写进 configurable 的 workspace_user / workspace_session,再按同一套哈希规则算根目录 (extensions/vent_airflow/workspace_paths.py)。

重写这套映射的风险是两处漂移,因此 tests/test_airflow_tools.py::test_workspace_path_parity_with_agents_layer 断言它与 agents.workspace.session_root 一致——漂移会变成失败的测试,而不是线上 「找不到文件」。注意 get_config() 在运行上下文之外(脚本、测试)抛 RuntimeError,必须捕获并回落到项目内目录。

坑 18:虚拟 /workspace 是会话沙箱,不是仓库目录

agent 看到的 /workspace/ 映射到 data/workspaces/<用户哈希>/<会话哈希>/workspace/ ——每个会话一份、互相隔离,由 backend_for_runtime 按需创建。仓库根目录下没有、 也不该有同名目录:_ensure_dirs() 曾经创建它(历史遗留),实测后果是使用者把矿方资料 放进仓库的 workspace/矿井风量研判资料/,然后让 agent 研判,agent 的 ls /workspace 返回空、给工具传路径报「文件不存在」,接着在文件系统里连查二十来个路径(/、/data、 /tmp、/files、全盘 glob…)全部 permission denied。

别再把它加回来。 资料进 agent 的通道只有两条:

  1. 对话上传 → /workspace/uploads/。/api/chat 除 1.5 兼容的单份 file 外还有 files 多附件字段(上限 TFAGENTS_MAX_UPLOAD_FILES)——研判至少要「配风计划 + 测风报表」两份,所以多附件字段是必需的。
  2. /skills/ 只放技能手册,不是资料。

另外技能文档里必须明确告诉模型这两条,并让它「找不到就请用户上传」;否则它会去翻文件系统。

坑 19:大对象不要塞进工具返回值

一次研判可能产出 92 个计划对象、95 条测风记录、上百条问题。把这些 JSON 全部 内联进工具返回值会:① 顶爆模型上下文,② 前端工具卡被截断成 4000 字符后只剩 半截 JSON。

extensions/vent_airflow/tools.py 的约定是「大对象落盘 + 紧凑摘要」:完整结果写到 会话工作区 outputs/airflow/<时间戳>-<名字>.json,返回值只带摘要、统计、内联前 AIRFLOW_MAX_INLINE_ITEMS 条,以及下游工具要用的 *_path。工具之间的传参因此 是文件路径,参数解析走 _load_argument(同时接受 JSON 字符串与路径)。

坑 20:模型中间件那条路拿不到会话身份,后端工厂必须自己回退

摘要压缩中间件需要后端时按框架自己的写法构造 runtime:

config = cast("RunnableConfig", getattr(runtime, "config", {}))

而 ModelRequest.runtime 的类型是 langgraph.runtime.Runtime——它没有 config 字段 (实测字段为 context / store / stream_writer / heartbeat / previous / execution_info / server_info / control),所以这里 config 恒为空字典,框架注释也明说空字典是合法取值。

backend_for_runtime 早先只判断「config 属性是否存在」,拿到 {} 就当有效配置,于是 直接抛 ValueError: 文件后端缺少服务端会话身份。症状与原因距离很远:长会话、或读了长 文档的子智能体,一触发摘要压缩就整轮失败,而不是压缩生效;短会话永远复现不了(压缩 没触发就没事)。真实触发场景是子智能体分块读 63 页配风计划 PDF 后压缩启动。

守则:

  • 会话身份把所有来源都试一遍(runtime.config → get_config()),都没有才算失败。 拿起一个空字典就判定"没有身份"是错的。
  • 失败仍要响:两个来源都拿不到身份时继续抛错——否则会悄悄写进别的会话目录。
  • 回归测试在 tests/test_workspace_backend.py:用 ToolRuntime(config={}) 在 langgraph 运行上下文里复现这条路径,不需要真模型。改 agents/workspace.py 必跑。
  • 当前 backend 传的是工厂,deepagents 已标记 deprecated(0.7.0 将移除)。届时仍需 保留「按运行身份解析沙箱根」的能力——那需要一个在内部读 get_config() 的后端实例, 而不是退回编译期固定的实例。

坑 21:模型传参的形态不可信,工具要「归一化 + 安全网」

真实故障:模型把 required_effective_window 写成字符串 "2024-12-01 ~ 2024-12-31", 而 compare.py 里是 dict(target.get(...) or {}) —— dict("…") 抛 ValueError: dictionary update sequence element #0 has length 1,异常一路穿过 ToolNode 打掉整轮 SSE 流(不是工具卡里报个错,是整个对话结束)。

两条纪律:

  1. 结构字段一律经 contracts.as_mapping / as_window / mappings 收口,不要直接 dict(...)。as_window 还能从 2024年12月1日、2024/12/01、2024-12-01 9:00 这类写法里把窗口捞回来(parse_time 做了非 ISO 归一化与补零),捞不回来就留空、 由调用方按"时间无法对齐"转人工复核。计划校验器对无法解析的生效窗口要给 unparsable_effective_window 问题,而不是放行。
  2. 工具必须挂 _readable_errors 安全网(extensions/vent_airflow/tool_support.py): 任何未预期异常都返回 {ok: false, error, hint} 并记日志,而不是抛出去。AGENTS §四 的「工具失败返回可读原因」在框架里是硬要求——抛异常的代价是整轮对话。

顺带记两条同源修正(都有测试守):

  • 阈值策略不能跨对象类型回退:workplace 缺 location_type 时要用工作面策略 (0.75),早先回落到回风系统的 1.0,等于把工作面按 100% 判定。
  • 时间不明就停止定性:ALIGN_MANUAL(窗口缺失或解析不出)与 ALIGN_FAILED 一样 只能输出人工复核——不知道实测是什么时候的,比了也没意义。
  • 逻辑 ID 与文件名分开:report_id 由 mine_id/period 拼成、两者都来自模型, 实测传过 2024-12(配风计划)/ 2026-05-28(实测),直接当文件名会因斜杠被当成路径 分隔符而写失败。落盘一律过 report._safe_filename,逻辑 ID 原样保留用于展示。

三、分层与依赖方向

main.py ──> api/ ──> agents/ ──> tools/
                └──> billing/ ──> db/
                └──> scheduler/
        所有层 ──> obs/ 与 config

extensions/ ──> tools/ 与 config(注册业务能力)
extensions/ ──> algorithms/(工具薄封装调确定性引擎)
algorithms/ ──> 不导入 api / agents / extensions

规则:

  • db/ 不反向依赖上层。业务逻辑不进 store,store 只做存取。
  • billing/ 不依赖 api/。计费只接受标量参数,不认识 StreamRequest。
  • tools/ 不依赖 agents/。工具是纯函数,拿不到 agent。需要会话上下文时 走 langgraph.config.get_config()(见坑 17)。
  • algorithms/ 不导入 api/、agents/、extensions/。确定性计算与解析放这里, 可以被脚本、HTTP 路由、扩展工具三处复用;Agent 侧由 extensions/ 注册工具调用它。
  • extensions/ 只做注册与薄封装。解析、复算、阈值、证据、渲染都在 algorithms/ 里;扩展文件不许出现风量公式或阈值数字。
  • obs/ 与 config 可以被任何层依赖,它们不依赖任何业务层。
  • 跨层取配置一律走 config,不要在业务代码里 os.getenv。

四、编码约定

  • 每个配置项必须有默认值,且默认值 = 「不配也能跑起来」的那套。 新增开关的默认值不得改变既有行为。
  • 对外契约的字段名只在 api/sse_events.py 声明。其他任何地方都不得 手拼 data: 字符串——字段改名漏掉一处,前端就静默渲染失败。
  • 契约只做加法:新增字段安全(旧前端忽略未知字段),改名或删除不安全 (静默破坏渲染)。要改已有字段名,先确认 1.5 前端也一起改。 唯一的例外是删掉 POST /api/chat/only(见 §六),那是一次经过评估的、 有记录的收缩——不是「删了也无所谓」的先例。 多附件字段 files 是这条纪律的正例:研判类任务一次要传「配风计划 + 测风报表」两份, 于是保留 1.5 的单份 file 不动、另加一个 files 数组字段,两者合并处理。旧前端 一个字段都不传也照常工作。
  • 函数式注释写「为什么」不写「是什么」。行内注释只在代码本身无法表达 约束时写(顺序要求、副作用、反直觉的取舍)。
  • 同步驱动(pymysql / sqlite3)在 async 路由里必须走 asyncio.to_thread, 否则单进程下一个人查历史会卡住所有人的流。
  • 工具失败返回可读原因,不抛裸异常。抛异常会打断整轮工具调用, 返回错误说明能让模型自己纠正。
  • 文件规模控制在 500 行以内(1.5 的约定)。超了就拆。

五、验证方式

# 1) 离线单元 + 契约测试(不需要模型密钥,不需要数据库服务)
python -m pytest

# 2) HTTP 层端到端(需要服务已启动 + 可用的模型密钥)
python main.py &                     # 或 bash scripts/start_server.sh start
python scripts/http_e2e.py

# 3) 只做导入与语法自检
python scripts/selfcheck.py

三者的分工:

检查 覆盖 何时跑
pytest 单元逻辑 + SSE 契约字段 + 内核通道分工(用假 agent) 每次改动
scripts/http_e2e.py 真实 HTTP + 真实模型 + 落库 + 中断闭环 + 调度 提交前、发布前
scripts/selfcheck.py 导入、建表、agent 编译、工具注册 改结构后快速自检

改 SSE 事件字段必须同步改 tests/test_sse_contract.py——那里的断言就是 兼容性契约本身。

风量研判另有三条专项回归,改到规则库、解析器或引擎时必须重跑:

断言 位置 守的是什么
真实报表读数与原始文件一致(总进风 35181、总回风 35713、主扇 22512.68 / 13716.14) tests/test_airflow_tools.py::test_real_measurement_report_totals_match_source 报表分块定位与页眉页脚识别
预处理产出的表格合计能对上计划声称的小计(9228 / 17853) test_real_plan_preprocess_is_faithful 视觉行序重建正确,名称与数值没错配
真实层级 fixture 的权威需风量精确为 11073.6 / 21423.6 / 32497.2 test_verify_real_table9_fixture_is_exact 汇总公式、备用系数与容差

真实资料 workspace/矿井风量研判资料/ 不入库,缺失时上述用例 skip;tests/fixtures/airflow/ 下的合成基准与转录 fixture 始终可用。


六、已知缺口(有意为之,不是遗漏)

列在这里是为了让读代码的人知道「这里没写不是因为忘了」。

为什么没有 /chat/only

1.5 有 POST /api/chat/only,2.0 删掉了。如果你正想「为了兼容 1.5 把它加 回来」,先看这段。

它和 /chat 的差别只有计费流水上那个 kind 标签(chat vs chat_only), 行为完全相同。而那个标签在 1.5 和 2.0 里都没有任何消费方:没有按渠道聚合 的接口、没有按渠道拆的额度、没有渠道维度的报表。也就是说它看起来像渠道区分, 实际不承载信息。

这份重复在 1.5 有可查的代价:

  • 出过计费 bug:docs/实施记录-B3-P3.md 的 R7-#24 / P1-11 —— 「/chat vs /chat/only 标题计费口径不一致」。两个本该一致的入口漂移了。
  • 多付一份安全验证:tests/l4_auth_runtime.py 里的 L4-1b-c2(thread_id 越权)与 L4-8c(413 体积闸)存在的唯一理由,是在第二个入口上再证一遍同样 的阀门。
  • 多养一个专用探针:tests/probe_b345_dedup_diff.py 的唯一职责是证明两者 行为相同。
  • 最终被收敛:B3-45 提交说明写着「/chat vs /chat/only 95% 重复,抽 _run_chat_pipeline」——1.5 自己也认定它是重复,只是出于兼容留了端点、只删 了重复实现。
  • 而 1.5 前端从头到尾 0 次调用 /chat/only。

要重新引入这类入口,正确的顺序是先把渠道差异做实(按渠道聚合用量、按渠道 拆额度、渠道维度报表),再引入入口;不要先立一个行为相同的端点,指望以后有人 去消费那个标签。

保留的另两个入口 —— /chat/resume、/chat/interrupt —— 不属于这类:它们承载 的是不同能力(中断恢复、挂起查询),不是同一能力的别名。

  1. 没有鉴权。/api/chat 直接取 multipart 的 user_name(缺省 default), 客户端可自报身份。计费、会话归属、并发配额全建立在这个值上,所以 当前只适合内网或开发环境。接真实鉴权时替换 api/chat_routes._resolve_user 为 Depends(...) 即可,其余代码不用动。
  2. 没有业务工具与业务技能。只有 4 个演示工具(时间/检索/计算/计划审批) 与 1 个示例技能。业务工具照 tools/demo_tools.py 的模子加。
  3. 没有 MCP 接入。1.5 的 tools/mcp_client_pool.py 等未移植。
  4. 会话标题用截断而非 LLM 生成(_ensure_session_title)。省一次模型 调用的钱和延迟;想换只改那一个函数体。
  5. 并发计数是进程内的。见不变式 2。
  6. remember 长期审批规则未实现。ResumeRequest.remember 收到即忽略。

风量研判(algorithms/airflow_review/ + extensions/vent_airflow/)

  1. 没有专用 HTTP 路由,研判结果不落库。报告与中间产物写在会话工作区 outputs/airflow/ 下,供对话内引用与 agent 用 read_file 回读;下载能力待 前端需求明确后再加。要做多矿批量,参照 algorithms/characteristic_curve_recognition/batch.py 的模式另建任务队列,不要往对话链路里塞。
  2. 安全监控没有真实数据源。extensions/vent_airflow/monitoring.py 只实现了本地 文件适配器与「无数据源 → 转人工复核」的降级路径;接真实系统时替换 get_source()。
  3. 配风计划解析依赖模型的提取环节。preprocess_plan_document 是确定性的 (清洗、行序重建、章节与表格识别、生成提取模板),但数值提取由模型按模板完成, 再经 validate_plan_extraction 校验。因此「关键字段提取准确率」跟模型能力相关; 要提高确定性就把提取模板继续细化(如把表格线索直接喂成待确认候选值)。
  4. 只支持有文本层的 PDF 与 docx/txt。扫描件与图片型 PDF 需要 OCR,本轮不做。
  5. 规则库只覆盖复算与阈值。形式审查(签字/编制时间)、红线清单(R1–R6)、 参数表比对、测风报表审核 6 条、以及云知识库的法定条款判定都不在范围内—— 这些在旧的 vent-plan-review skill 里有,属另一条产品线。

七、改动清单模板

动到以下任一文件时,想一想上面哪条纪律相关:

文件 相关纪律
api/sse_events.py 契约唯一出口(约定 §4)+ 契约测试必须同步
api/chat_routes.py 契约只做加法(§4);附件通道与 file/files 合并见坑 18,测试在 tests/test_chat_uploads.py
agents/agent_factory.py / agents/workspace.py 坑 10(权限首条命中即返回)、坑 18(虚拟 /workspace 是会话沙箱;不要创建仓库级同名目录)、坑 20(后端工厂必须回退到运行上下文取会话身份,否则压缩一触发就崩)
api/sse_core.py 坑 5 / 坑 6 / 坑 9、不变式 1、3、4
agents/agent_factory.py 不变式 5、坑 2、坑 3、坑 10
config.py 坑 1、每项必须有默认值
agents/agent_memory.py 不变式 2、坑 2
tools/registry.py 不变式 4、坑 7
billing/* 不变式 3、坑 9
db/* 坑 8
main.py 坑 11、不变式 2
algorithms/airflow_review/rules/*.json 坑 16(公式变体与 bounds);改公式必须同步重跑三条专项回归
algorithms/airflow_review/parsers/plan_preprocess.py 坑 14、坑 15;改行序重建必须重跑 9228/17853 断言
algorithms/airflow_review/engine/* 坑 15(待检容器不去重);改阈值/容差必须同步 tests/test_airflow_engine.py 的九类基准
extensions/vent_airflow/workspace_paths.py 坑 17;路径映射必须与 agents.workspace 保持一致(有 parity 测试守)
extensions/vent_airflow/tools.py 坑 18(大对象落盘)、AGENTS §四「工具失败返回可读原因」
extensions/vent_airflow/extension.py 扩展只做注册;工具名或参数变更要同步 skills/airflow-review/references/contracts/tools.md(有测试守一致性)