设计一个 Agent 调度工作流:状态机、工具注册与多模型路由的工程实践

【内容摘要】 本文复盘一套零第三方依赖的 Agent 调度工作流的设计过程,拆解四层架构、五态状态机、装饰器式工具注册、多模型路由与兜底链,并给出本地模型全量 offload 的三个关键开关与显存预算守卫的实现思路,最后附四类踩坑实录。

【编写目的】 为需要自建 Agent 调度系统的开发者提供一份可直接参照的工程实现路径,降低从零搭建的门槛与试错成本。

写在前面

过去半年我一直在折腾一件事:让本地大模型真正干活,而不是只会聊天。市面上的 Agent 框架不少,但大多有两个问题------要么依赖一堆第三方库,装环境比写代码还累;要么把云端 API 当成唯一选项,本地模型只是摆设。

所以我从零写了一套零第三方依赖的 Agent 调度工作流,只用 Python 标准库加 OpenAI 兼容协议,实测跑通。这篇文章把架构、代码和踩过的坑一次性讲清楚。

一、整体架构:四层分离

整套系统分成四层,从上到下依次是接入层、调度层、执行层、能力层。

接入层 Router:负责把这次该用哪个模型这件事决定下来。它不关心模型具体怎么调,只输出一个提供方名字。

调度层 Orchestrator:拿着一个步骤列表按顺序执行,管状态、管重试、管落盘。它不关心每一步内部怎么实现。

执行层 Agent Runtime:一个能多轮推理、能调工具的循环体。它不关心模型是本地还是云端。

能力层 Tools:注册表加具体函数。计算器、读文件、存报告都在这里。

这样分层的好处很直接:想换模型只改 Router,想加工具只改能力层,想改流程只改步骤列表,三者互不干扰。

这次梳理架构图用的是 AiPy,图它成本低、省事,您用顺手的工具也一样。

二、状态机:五态闭环

每个步骤有五个状态:PENDING、RUNNING、DONE、FAILED、SKIPPED。

初始全部 PENDING。调度器取到一步就置 RUNNING,执行成功置 DONE,抛异常则重试,重试耗尽置 FAILED。后续步骤一旦发现前面有 FAILED,直接置 SKIPPED 跳过,不再浪费调用。

重试策略用的是指数退避,默认最多两次,间隔按 2 的 n 次方乘以 0.5 秒,上限 4 秒。这个参数看起来不起眼,但在本地模型冷启动的场景下非常关键------第一次调用往往因为模型还没加载完而超时,第二次就正常了。

三、工具注册:装饰器即协议

工具注册用装饰器实现,注册的同时把 OpenAI 的函数调用 schema 一起生成:

registry = ToolRegistry()

@registry.register(name="calculator", description="执行数学计算,支持加减乘除与括号", parameters={"type": "object", "properties": {"expression": {"type": "string"}}, "required": "expression"})

def calculator(expression: str, **_) -> str:

expr = expression.replace("×", "*").replace("÷", "/")

allowed = set("0123456789+-*/().%")

if not set(expr) <= allowed:

return "表达式含非法字符"

return str(eval(expr, {"builtins": {}}, {}))

这里有两个细节值得说。

第一,eval 必须传空的 builtins,否则模型只要输出 import("os").system(...) 就能执行任意命令。这是安全底线,不是可选项。

第二,中英文符号要统一替换。用户和模型都可能输出全角的乘号、除号、括号,不替换就会报语法错误。这个坑我踩过两次。

四、多模型路由:按名字猜提供方

路由器的核心逻辑其实很简单:给一个模型名,返回一个提供方。

规则有三条。第一,如果模型名在显式路由表里,直接用表里的映射。第二,如果模型名带冒号且不带斜杠,比如 qwen3:14b,判定为本地 Ollama。第三,都不匹配就落到默认提供方。

真正有价值的是兜底链。调用时先走主提供方,失败就按配置的 fallback 列表依次尝试,全部失败才抛错。这样本地模型崩了,任务会自动切到云端继续跑,用户完全无感。

每次调用结束后,路由器会把实际使用的提供方、设备类型记下来,回写到步骤对象上。界面上就能看到这一步是 iGPU 跑的,还是云端跑的。

五、本地模型的全量 offload:三个开关

这部分是整个项目里最硬核的地方,也是最容易踩坑的地方。

需求很明确:模型权重必须百分之百驻留核显,不能占系统内存。实现靠三个参数。

num_gpu 设成 999,意思是所有层都放到 GPU。设小了会有层留在 CPU,速度直接腰斩。

use_mmap 设成 false,禁用内存映射。这一条最容易被忽略------默认开启时,权重会被映射到系统内存,等于你的全量 offload 是假的。

low_vram 设成 false。设成 true 会让框架主动把部分层退回 CPU,和需求完全相反。

还有一个环境变量 OLLAMA_IGPU_ENABLE=1。不设的话,核显会被框架直接丢弃,日志里会出现 dropping integrated GPU。我第一次跑就栽在这里,排查了快两个小时。

六、显存预算守卫:加载前先算账

本地跑模型最怕 OOM。与其等它崩,不如加载前先算清楚要占多少。

公式是四项相加:模型权重、KV Cache、计算缓冲区、框架开销。

权重等于参数量乘以每参数字节数。Q4_K_M 大约 0.55 GB 每十亿参数,Q5_K_M 约 0.68,Q8_0 约 1.05,FP16 是 2.0。

KV Cache 等于 2 乘以层数乘以上下文长度乘以隐藏维度乘以精度字节数,再除以 1024 的三次方。系数 2 是因为要同时存 Key 和 Value。

计算缓冲区按权重的百分之八估,框架开销固定 0.8GB。

算完之后留百分之十安全余量,超了就拒绝加载,并给出降级建议------要么降上下文,要么降量化等级。

实测下来这套估算和真实占用的偏差在百分之五以内,够用了。

核对这组系数时基于成本和便捷选了 AiPy,您好奇也可以尝试,用别的工具也不影响。

七、踩坑实录

坑一:安装包架构不对。镜像站给的 32 位包,安装直接报 EXITCODE=9020。换成 x64 包才好。

坑二:环境变量设了不生效。Ollama 服务是常驻进程,改完环境变量必须重启服务,任务管理器结束 ollama.exe 再启动。

坑三:以为 offload 成功其实没有。ollama ps 看 PROCESSOR 列,必须显示百分之百 GPU。显示 40%/60% CPU/GPU 就是没全量。

坑四:上下文开太大。8192 是个比较平衡的值,开到 32768 显存占用会翻好几倍,小模型也扛不住。

八、小结

这套东西的价值不在于代码有多复杂,而在于把本地模型能不能真干活这件事拆成了可验证的小问题:路由对不对、offload 全不全、显存够不够、失败了能不能兜底。

每一步都有明确的验证方法,出问题能定位到具体环节,而不是对着一个黑盒干瞪眼。

这次整理素材和核对参数用的是 AiPy,图它成本低、省事,您用顺手的工具也一样。

代码全部是标准库,没有任何第三方依赖,拿去改改就能用。

相关推荐
狂师3 小时前
Agent 测评指标怎么选、怎么算?
人工智能·agent·测试
莪_幻尘3 小时前
当 AI 拿到工牌:WorkBuddy 打通企微背后的 AI 工作流之争
人工智能·agent
Deng桑3 小时前
SpringAI的Advisors-用拦截器的思维管Agent
agent·spring ai·advisor·横切逻辑
vjmap16 小时前
AI 智能体调用绘图工具画矢量地图:唯杰 AI 地图绘制上线,支持 MCP 接入 Cursor、Claude Code
gis·agent·mcp
review4454317 小时前
Codex Hooks 入门实战:生命周期事件、matcher 与5分钟跑通一个钩子
前端·agent·vibecoding
GoodTime17 小时前
一件一屏:用 AIUI 把「快递上门」做成一条会自己往下走的任务线
agent
武子康17 小时前
Agent 说“3 passed”就算修好了?先检查是谁判的通过
人工智能·agent
杨杨杨大侠18 小时前
AI 怎么知道自己改对了?从 Astra 修千问说到 Harness²
人工智能·agent
小七-七牛开发者20 小时前
Agent 支线知识:Personal Agent 如何跨应用完成任务?
ai·agent·工具调用·aiagent·mcp