当前主通风机批次使用固定工作流:Python/PDFium 文本提取、GLM OCR 扫描页识别、Qwen3.5 单次 JSON 研判。服务器本地测试保持两个推送开关均为 False。
镜像含 Python 3.12、锁定的依赖、前端、预缓存的分词词表及 Linux bubblewrap。单进程运行;数据库、workspace、输出和日志分别通过卷持久化。.env、运行数据及 PDF 不进入镜像。修改配置后重建容器生效。
复制 .env.example 为 .env,填写模型、网关、BUSINESS_API_TOKEN 和需要的结果平台配置。保留既有真实配置时不覆盖它。SHANXI_GATEWAY_ENABLED=True 后配置真实网关 URL/token,才能启动批次服务。
本部署设置 BILLING_ENABLED=False:不登记模型单价、不计算费用,保留每次请求的输入/输出 Token、模型及执行记录。无需配置 BILLING_ALLOW_UNPRICED;Token 用量持久化和可选 Token 限额仍保留。
默认思考级别为 DEEPAGENT_THINKING=highest。Qwen3.5 使用 vLLM 的 chat_template_kwargs.enable_thinking:off 显式传 false,high/highest 显式传 true;该模型接口没有独立的最高深度档位,不发送 reasoning_effort=highest。模型识别兼容 Qwen/Qwen3.5-35B-A3B-FP8 等带组织前缀、量化后缀的名称,并保留实际请求模型名。GET /api/model 的 thinking_parameters 和启动日志显示请求使用的思考开关;它们不代替模型服务响应的核验。
保留旧配置部署时,把原 .env 的 DEEPAGENT_THINKING 改为 highest。使用 docker run --env-file .env.docker 时还须重新导出并重建容器,或在创建容器时显式加 -e DEEPAGENT_THINKING=highest;旧 env-file 中的 off 会覆盖镜像默认值。
Dockerfile 的结果推送开关默认均关闭:
ENV RESULT_PLATFORM_ENABLED=False
ENV MAIN_FAN_WORKFLOW_PUBLISH_ENABLED=False
| 值 | 批次行为 |
|---|---|
False(默认) |
报告、结果 JSON、Agent 输入和证据均落本地,不启动结果推送服务。 |
True |
启用结果平台服务;新工作流还须显式开启 MAIN_FAN_WORKFLOW_PUBLISH_ENABLED=True 才会将本轮结果入队。 |
开启时还需配置 RESULT_PLATFORM_URL、鉴权及成功回执字段。未完成结果记录为 skipped_incomplete,不会伪装成正式完成结果发送。网络错误不删除本地结果;回执、次数、下一次重试时间保存在业务数据库,按 推送记录.json 的 task_id 查询业务任务。目录里的状态是入队时快照,以业务任务记录为最新状态。总开关开启后,历史已入队的失败/待重试任务也会按现有队列规则恢复;原来关闭时未入队的本地批次不会自动补推。
网页/工具会话的自动推送还沿用 MAIN_FAN_AUTO_PUBLISH;新批次要求总开关和工作流推送开关同时开启。显式提交任务 API 仍须 publish=true。scripts/run_main_fan_batch.py 是隔离本地工作流入口,始终禁止推送;不传 --mock-pdf 时使用真实资料服务。服务器本地测试应保持总开关关闭,避免历史队列在后台恢复推送。
在项目根目录操作。以下为部署时执行的命令,本次开发没有执行构建或启动。
docker build -t ventagent:local .
docker save -o ventagent.tar ventagent:local
将镜像、compose.yaml 和配置放到服务器后:
docker load -i ventagent.tar
docker compose up -d --no-build --pull never
docker compose logs -f --tail=100 ventagent
镜像架构必须匹配服务器;在 Apple Silicon 为 x86 服务器打包时使用 docker buildx build --platform linux/amd64 --load -t ventagent:local .。Compose 使用宿主机网络,供 Linux 服务器部署,服务端口为 8080。
联网机器负责构建,服务器只导入、运行。服务器不需要另外安装 Python/npm,也不需要执行 pip/apt 或拉取基础镜像。以上 --no-build --pull never 禁止在服务器构建或拉取镜像。运行仍依赖可达的内网模型、取数网关、报告采集服务;推送开启时也须访问结果平台。
先在服务器执行下面的只读命令,确认架构和 Docker 条件:
uname -m
docker --version
docker compose version
x86_64 对应构建参数 --platform linux/amd64,aarch64 对应 --platform linux/arm64。将导出的 ventagent.tar、compose.yaml 和已填写的 .env 放到服务器同一个目录,切到该目录执行上面的导入和启动命令。源码和 Dockerfile 不必传到服务器;.env 要单独传,权限建议设为 600。若服务器上已经有这些文件或卷,保留原有配置和数据。
资料采集服务与 VentAgent 在同一台服务器且只监听回环时,服务器 .env 显式设置 MAIN_FAN_REPORT_API_URL=http://127.0.0.1:19010;保留既有内网模型/网关地址和凭证。确认 /home/chief/source_data 存在且 UID 10001 可读,挂载配置沿用本文说明。启动后会自动运行限量批次,之后按月度规则调度。
如果没有 Docker 或 Compose,需先为目标服务器的发行版、版本和 CPU 架构准备离线安装包及依赖;镜像 tar 不包含 Docker 引擎。此时还需提供 cat /etc/os-release 的输出,不能直接混用其他系统的 rpm/deb 包。
Compose 使用非 root 用户(UID/GID 10001);/home/chief/source_data 及父目录须允许该用户遍历、读取。不要将整棵服务器目录挂载进应用。命名卷保存应用可写数据。bubblewrap 依赖宿主机开启非特权用户命名空间;Compose 放开容器 seccomp/AppArmor 对命名空间的限制,未授予 SYS_ADMIN,应用执行的 Python 仍进入现有禁网沙箱。该运行配置需要在目标服务器验证;若宿主机禁用用户命名空间,需调整宿主策略,程序不会退回无沙箱执行。
docker run --env-file 按字面值读文件,不能直接复用含外层引号、内联注释的 dotenv 文件。模型名变成 "'Qwen/...'"、True 开关失效就是这一差异。可在有项目环境的电脑先运行 .venv/bin/python scripts/export_docker_env.py .env .env.docker,再把权限0600的 .env.docker 传到服务器;脚本不修改原文件、不打印配置值,也不启动应用。
服务器没有 Python 时,已导入的镜像就能转换。下面每条命令都是独立的一整行;在 .env 所在目录执行。转换成功后才继续启动,原 .env 保留:
umask 077
docker run --rm --pull never --network none --user 0 --mount "type=bind,src=$PWD/.env,dst=/input.env,readonly" --entrypoint python ventagent:local -c 'from dotenv import dotenv_values; import sys; rows=dotenv_values("/input.env"); assert all(v is None or not any(c in v for c in "\r\n\x00") for v in rows.values()), "配置含有不支持的多行值"; sys.stdout.write("".join(f"{k}={v}\n" for k,v in rows.items() if v is not None)); print("配置转换完成", file=sys.stderr)' > .env.docker
chmod 600 .env.docker
下面启动命令沿用持久化卷、只读报告挂载和现有沙箱配置,显式启用主通风机及网关。网关 URL/token、模型凭证需已在原 .env 配置;不输出凭证。资料服务和应用同机时用回环地址,否则将 MAIN_FAN_REPORT_API_URL 改成实际地址。仅首次创建容器时执行;已有同名容器时先停止并删除容器,保留命名卷。
docker run -d --name ventagent --pull never --init --restart unless-stopped --network host --env-file .env.docker -e DEEP_AGENT_ENV_PRIORITY=process -e MAIN_FAN_ENABLED=True -e SHANXI_GATEWAY_ENABLED=True -e MAIN_FAN_LOCAL_BATCH_ENABLED=True -e MAIN_FAN_REPORT_API_URL=http://127.0.0.1:19010 --security-opt seccomp=unconfined --security-opt apparmor=unconfined --cap-drop ALL --log-driver json-file --log-opt max-size=20m --log-opt max-file=5 -v ventagent-data:/app/data -v ventagent-outputs:/app/outputs -v ventagent-logs:/app/logs --mount type=bind,src=/home/chief/source_data,dst=/data/source_data,readonly ventagent:local
旧镜像曾在预热时创建 /app/workspace 和 /app/conversation_history,非 root 用户无权创建源码根目录的子目录。新源码已修正:只读技能,实际工作区及历史使用 /app/data/workspaces 持久化卷。暂不重打旧镜像时,在上面的启动命令额外加 --tmpfs /app/workspace:rw,uid=10001,gid=10001,mode=0700 --tmpfs /app/conversation_history:rw,uid=10001,gid=10001,mode=0700,兼容旧初始化;这两个旧共享目录不存放实际会话数据。
修改 .env 后重新导出 .env.docker 并重新创建容器,单纯 docker restart 不更新容器环境。以 env-file 注入时不要求存在容器内 /app/.env;缺少这个文件不代表注入失败,模型名是否无引号、扩展是否启用及启动批次日志才是有效检查项。
服务器 /home/chief/source_data(只读挂载)
→ 容器 /data/source_data/{矿名全称}/{年}/通风机性能测定报告/*.pdf
→ /app/data/workspaces/.../workspace/exploration/{workflow_id}/reports/report-N.pdf
→ 给Agent的数据.json 的 reports[].path
默认文件服务 URL 为 http://10.244.3.155:19010。快速调用文档说明服务仅监听宿主机回环,所以 Compose 的默认值为 http://127.0.0.1:19010 配合宿主机网络;若已开放内网监听,可在 .env 设置 MAIN_FAN_REPORT_API_URL=http://10.244.3.155:19010。目录挂载不能解决 API 监听问题。
固定代码依次调用 /mines?with_tree=0 核对同一行 dept_id 和矿名全称、/doc-types/sync、/doc-types 动态获取资料类型号、/crawl,再轮询 /crawl/{task_id}。每次一个矿、一种类型、一期,多个通风井复用矿级采集结果。MAIN_FAN_REPORT_PERIOD 留空取批次截止月份;可显式设为 2026-03,不写死示例类型号 129。
快速文档未定义返回附件路径清单,因此完成后只读取约定的同矿、同年、同资料类型目录内的直接 PDF 文件;它们可能包括往期原件,不能声称全是本期新增文件,报告日期及通风井/风机归属仍核对正文。当前不扫描子目录,也不解压附件;非 PDF 不送入这条 PDF 分析链。目录不存在则报告集合为空;存在但不可读、文件非法、超限、采集 partial/failed 都明确失败,不静默回退到测试 PDF。report-source/task.json 保存任务 ID 和响应供恢复、排查,files.json 保存原件相对路径。哈希使用复制后的实际内容,命中同矿同井档案则复用。
报告查询示例(Agent 虚拟路径,宿主实际文件位于持久化 workspace):
{"reports":[{"path":"/workspace/exploration/<workflow_id>/reports/report-1.pdf","mode":"analyze","sha256":"<实际文件hash>","page_count":12,"binding":"mine_directory_content_unverified"}]}
所有通用和业务 Agent 均禁用子 Agent:不挂载扩展子 Agent,并在实际模型的 HarnessProfile 中显式关闭框架默认添加的 general-purpose。模型不再获得 task 工具,系统提示词也不再包含委派指引。GET /api/model 的 agent.subagents 返回空列表,agent.subagents_enabled 为 false。
主通风机批次、业务任务 API 和网关触发的独立业务入口保留,由当前业务 Agent 自行读取资料、执行 Python 并提交报告。通用聊天入口不再通过子 Agent 分发专业业务。此项为源码中的全局禁用策略,没有启用子 Agent 的环境变量;更新镜像后重新创建容器生效。
默认同时输出 stdout 和 /app/logs/ventagent.jsonl,每个文件 20MB,保留 5 个轮转文件;Docker stdout 也限制大小。设置 LOG_LEVEL=DEBUG 可查看各接口耗时、每日时序查询及 PDF 复制记录;INFO 保留阶段汇总、报告 hash 复用数量和模型 token 用量。
检索字段包括 batch_id、dept_id、shaft_id、workflow_id、crawl_task_id、推送 task_id 和 event。错误带堆栈,日志沿用凭证脱敏,不记录 PDF 内容和整批响应。模型调用前失败时,本地 failure.json 和 report-source/task.json 可辅助定位。
下面的 SDK 重试说明适用于原 Agent 入口。新批次固定工作流只发一次 Qwen 生成请求,429 也直接记录失败,不受 OPENAI_MAX_RETRIES 影响,不会重新执行或多次研判。
这表示模型服务的并发名额已满。主通风机批次队列按矿井、通风井串行执行,但模型账号或服务还可能被网页对话、其他实例或其他调用方占用;仅凭应用日志无法确定占用者。
模型请求通过 OPENAI_MAX_RETRIES 配置 SDK 退避重试次数,默认 8 次(不含首次请求),可设为 0 禁用。没有服务端等待提示时,当前锁定 SDK 的 8 次重试累计等待约 30–40 秒,另加各次请求本身耗时;存在受支持的 Retry-After 时按该提示等待。重试覆盖尚未成功建立响应的 HTTP 请求,已开始的流式响应失败仍会明确报错,不重放已生成内容或已执行工具。非临时错误继续按 SDK 规则直接失败。
这项设置需要使用包含新代码的镜像;仅给旧镜像添加环境变量不会改变写死的 2 次重试。配置变更后重新创建容器。若等待后仍持续返回 concurrent_limit,需模型服务方根据请求 ID 核查并发配额和占用;增加重试次数不能增加上游名额。
2026-10-22 的现场记录中,已失败矿井的报告采集和 240 个历史日段均已完成;首次模型响应却只有 finish_reason=tool_calls,没有工具名和参数。直接请求同一模型接口确认:流式模式的 170 个事件全部为 delta={};非流式模式返回完整的工具调用及实际用量。这是模型接口的流式工具返回异常,清空会话或增加 HTTP 重试次数不能补回这些字段。
镜像默认设置 OPENAI_TOOL_CALL_STREAMING=False,带工具的模型调用读取完整 JSON 响应,由 LangChain 正常解析、执行工具;应用的 SSE 消息和进度事件保留。模型就绪日志中的 tool_call_streaming=False 可确认开关生效。源码运行可配置同名环境变量;改动后重新创建容器或重启应用以重建 Agent。接入已确认支持完整工具流的模型服务后可设为 True。
同一模型网关对助手消息的 content 只接受字符串或列表。LangChain 在助手只有工具调用时会发送 content: null,因此首次请求可以返回工具调用,带上工具结果的下一次请求却被网关拒绝。直接请求复现了 messages[1].content 类型校验失败;只将该字段改为 "",同一请求即返回 HTTP 200 和完整工具调用。
配置 OPENAI_BASE_URL 时,模型工厂使用请求格式适配类,在序列化后将空工具调用消息的 content: null 改为 content: ""。适配覆盖同步、异步和流式请求,保留工具名、参数、调用 ID 以及工具返回内容,不修改已有会话和检查点。此问题不需要清空记忆;需使用包含适配代码的新镜像并重新创建容器。
每个通风井的结果目录为 /app/outputs/main_fan_batches/{批次ID}/{deptId}/{shaftId}/,其中保留:
报告.md
结果.json
给Agent的数据.json
给Qwen的数据.json
PDF提取结果.json
Qwen原始响应.json
待审阅报文.json
研判数据.zip
数据清单.json
来源与覆盖.json
推送记录.json
研判数据.zip 包含本次实际绑定的输入、PDF 原件、历史数据证据压缩包、逐页文字及 OCR 图片/请求/响应、唯一一次 Qwen 的完整请求和原始响应。给Qwen的数据.json 是实际完整研判输入,给Agent的数据.json 兼容保留原监测预览。压缩包内的 workspace/ 对应会话 /workspace/;原始日段和完整采样继续放在 history-evidence.zip 内。
数据清单.json 记录原路径、压缩包内路径、大小、SHA256、矿井和工作流归属。原始输入按分析时的指纹严格核验;被脚本覆盖或删除的中间版本会列在 unavailable_intermediates,不会用当前同名内容冒充旧内容。先完成结果和数据归档,再根据推送开关投递;归档失败后恢复只重试导出,不重新调用 Agent。已有数据还保留在矿级工作区,方便排查中断任务。
在服务器 .env 中配置:
MAIN_FAN_LOCAL_BATCH_ENABLED=True
MAIN_FAN_BATCH_RUN_ON_STARTUP=False
MAIN_FAN_BATCH_STARTUP_MAX_MINES=2
MAIN_FAN_BATCH_CRON=0 2 1 * *
MAIN_FAN_BATCH_SPREAD_DAYS=20
MAIN_FAN_BATCH_DEPT_IDS=all
MAIN_FAN_BATCH_ALLOW_ALL=True
MAIN_FAN_BATCH_MAX_MINES=0
北京时间每月 1 日 02:00 查询并冻结当月正常生产煤矿清单,按清单顺序均分为 20 份,第 1~20 天每天 02:00 投递一份,份内仍逐矿串行处理。总矿数除以 20 的余数分到前几天,每日矿数最多相差 1;例如 403 矿为前 3 天每天 21 矿、后 17 天每天 20 矿。少于 20 矿时前 N 天每天 1 矿,后续空份跳过;没有矿井时只保存空计划。
MAIN_FAN_BATCH_SPREAD_DAYS 支持 1~28,未配置时为 1,保留原有每月整批执行方式;部署模板为 20。月度触发仍由 MAIN_FAN_BATCH_CRON 控制,日调度取其中相同的小时、分钟,每天检查已有计划,不会重新获取清单或重新划分。月度计划保存在 MAIN_FAN_BATCH_OUTPUT_DIR/monthly_plans/{plan_id}.json,记录清单快照、每日矿井、原计划时间和批次标识。重复触发同月同范围时复用原计划;改变天数不会重新划分已有计划。每个每日批次在实际执行时冻结自己的监测截止时间,默认回查当时最近 40 天。
每个北京时间自然日最多领取一份,完成后重复触发也不会提前领取下一份。服务重启会恢复已投递但中断的份额,复用原批次目录和已完成结果;恢复当天不再增加新份额。上一份超过一天仍未结束时,不堆积后续批次。停机漏掉的份额在后续日调度按原顺序每天一份顺延,可能超过 20 日;跨月时先处理旧计划,新旧计划共用每日限额。某份执行结束但有失败矿井(partial/failed)时保留失败记录,下一天继续下一份,不自动无限重试失败矿井。
仅月度触发器创建新计划;月中首次部署或停机错过月度触发时,启动和日调度不会自动补建整月计划。清空 MAIN_FAN_BATCH_CRON 同时关闭月度触发、日调度和已中断份额的启动自动恢复。日志 batch.monthly.plan.created 记录每日矿数,batch.monthly.day.queued 记录实际投递日期、份号和批次标识。
MAIN_FAN_BATCH_MAX_MINES=0 不额外限制实际批次矿数;设为正数时,分日模式按每份矿数校验,超过上限就报错,不截断清单。MAIN_FAN_BATCH_DEPT_IDS 也可指定逗号分隔的部门编号,按该范围均分。全矿模式仍须 MAIN_FAN_BATCH_ALLOW_ALL=True。
如需独立的启动研判,可另设 MAIN_FAN_BATCH_RUN_ON_STARTUP=True,服务就绪后从同一候选范围取前 MAIN_FAN_BATCH_STARTUP_MAX_MINES 个矿,每次重启都会额外跑一批,与月度计划分别计数。该数量必须大于 0,也受正数 MAIN_FAN_BATCH_MAX_MINES 保护上限约束。分日部署建议关闭独立启动批次;False 仍允许恢复已有月度计划中已投递的中断份额。
改完 .env 需使用包含分日代码的新镜像并重新创建容器才加载配置。独立本地预览脚本不启动调度器,因此不会额外触发自动批次。