QEMU 训练营 2026 项目阶段:Agent 自动化外设建模¶
主要贡献者
这是一篇阶段性总结。前半部分回顾 K230 RMU 与 STM32F103 GPIO 两项外设建模工作,以及我第一次完整参与 QEMU 上游协作的过程;后半部分总结
qemu-agent从通用 Agent 原型走向 QEMU 专用建模 Agent 的三次架构演进。
从两个外设模型开始理解上游开发¶
项目阶段的第一条主线,是为 QEMU 补充 K230 RMU 和 STM32F103 GPIO 设备模型。单看代码规模,它们都不算庞大;但真正困难的部分并不是“把寄存器写进一个数组”, 而是把分散在 TRM、Linux 驱动、现有 QEMU 设备和 review 邮件中的信息,收敛成一组可实现、 可验证、也能被上游维护者接受的硬件语义。
K230 RMU:从寄存器映射走向复位语义¶
K230 RMU 的第一版确实以 MMIO 寄存器映射为主体,但模型不能停留在“地址可读写”。 RMU 寄存器同时包含写选通、复位请求、完成状态、W1C、自清和普通存储位;同一个 32 位值中, 不同位的访问权限和状态迁移并不相同。实现时需要先把这些位归纳为掩码和行为类别,再让 read/write 路径依据描述表执行,而不是为每个寄存器堆叠分支。
更重要的是,设备类型注册并不等于设备已经存在于机器中。模型还需要完成 Kconfig 与 Meson 接入、在 K230 SoC 中实例化、映射 MMIO,并移除同地址的 unimplemented stub。 qtest 则从进程外直接驱动 MMIO,验证复位值、写选通、完成位清除和重复复位等行为。 这让我第一次完整理解一个 QEMU 外设的交付面:设备实现、构建系统、板级接线、trace、 测试和 MAINTAINERS 缺一不可。
第一轮 review 也暴露了资料选择的问题。我最初依据网络上的二手信息整理寄存器,后来 reviewer
君泽给出了准确的 K230 TRM,并对照主线 Linux reset-k230 驱动指出模型仍需处理真实复位传播、
文档复位值、保留位与只读位掩码,以及 CPU1 assert/deassert 等语义。这个过程改变了我的
建模顺序:先用驱动确认 guest 实际依赖的访问序列,再用 TRM 确认复位值、位定义和时序,
最后才把行为落入 QEMU 模型。资料之间发生冲突时,不能依靠直觉补全。
STM32F103 GPIO:复用的是语义,不是代码形状¶
STM32F103 GPIO 的核心思路来自已有 STM32 GPIO 模型,但 F1 与 F4 的寄存器布局并不相同。 F1 使用 CRL/CRH 为每个引脚编码模式和配置,因此不能简单复制另一代芯片的寄存器实现。 我先把 F1 的位编码转换成“输入/输出、推挽/开漏、上拉/下拉”等语义标识,再复用已经验证的 引脚电平更新思路。这里真正可复用的是硬件语义和状态迁移,而不是寄存器字段本身。
GPIO 模型也让我更清楚地看到“寄存器正确”与“外部可观察行为正确”之间的区别。例如 reset 不能只把 IDR/ODR 清零,还必须让对外 GPIO 线同步回到正确电平;测试也不能只读回寄存器, 还要观察输出线。后续 review 中出现的 reset 输出未更新、Rust 构建依赖、SoC 接线以及提交中 混入无关格式化等问题,都说明设备模型需要同时接受硬件语义、QEMU 对象模型和工程边界三方面检查。
比代码更重要的是完整的上游闭环¶
这两项工作的最大收获,并不是完成了多复杂的算法,而是第一次走通 QEMU 邮件列表的协作闭环:
- 从 TRM、驱动和相邻设备中建立硬件事实;
- 把模型、接线和测试拆成可独立理解的提交;
- 运行相关 target 构建、qtest、checkpatch 和
get_maintainer.pl; - 使用
git format-patch/git send-email发送 series; - 在邮件线程中逐条回应 review,并在 v2 中说明修改与验证结果。
过去我更多是在成熟框架和既定 API 中补充局部实现。这次经历迫使我为每一个非直觉行为找到 依据,也让我开始把后端开发中形成的接口、状态和测试思维迁移到底层建模:寄存器描述表是数据 驱动的规则,qtest 是跨进程的行为验证,而 patch series 本身也是需要设计的交付接口。 从这个意义上说,我真正获得的不是“写完两个外设”,而是从建模练习者迈出了成为上游贡献者的一步。
从建模经验到自动化工具链¶
项目地址:https://github.com/jack-wang-176/qemu-agent。
写下这篇总结时,项目仍处于重构阶段。它已经能够运行 Agent loop 和外设建模流水线, 但还不能被描述成一个成熟的“自动建模系统”。目前更准确的定位,是一套面向 QEMU 外设建模的实验性工具链:从硬件资料中提取寄存器语义,形成 Reg-IR,生成可审查的代码产物, 再经过人工批准、构建和 qtest 验证后落入 QEMU 源码树。
项目背景以及我选择这个项目的原因¶
我在大一阶段的主要学习方向是 Go Web 后端。参加训练营时,我希望把自己已有的工程能力与 刚接触的 QEMU 建模知识结合起来,而不是再做一个与训练营主题关系不大的通用聊天机器人。 两个外设 patch 的经历也提供了一个具体问题:建模中最耗时的部分,往往是资料筛选、寄存器语义 整理、代码骨架生成、测试设计和反复 review。LLM 可以参与这些步骤,但前提是把它放进一个 受约束、可追踪、允许人工纠错的工程流程中。
项目最初只是一个简单的 Agent loop,随后逐步加入模型切换、工具调用、session、memory、skill、 CLI 与事件流,最后才把五阶段 modeling 命令接入应用。功能增长带来了一个明显问题:通用 Agent 和建模流水线虽然存在于同一进程,却没有共享一套真正统一的执行语义。对话使用 session 和 memory,建模则更像 CLI 嵌入的独立程序。后面三次重构,正是在不断修正这个边界。
第一次项目重构¶
第一次重构发生在项目规模还较小时,目标是阻止 OpenAI SDK 的类型继续向 Agent、session 和
tool 层扩散。我在 internal/llm 中定义项目自己的 Message、Request、Response、
ToolCall 和 ToolSchema,并用 Provider 接口约束模型调用。OpenAI 兼容实现只在 llm
边界完成内部结构与 SDK union 类型之间的转换。这样,后续增加 OpenRouter、OpenAI 或 Ollama
时,上层只面对统一的 provider 能力和模型引用,而不需要理解某个 SDK 的具体消息结构。
工具层则增加了一套小型 Schema DSL。工具通过 schema.Object、schema.Required、
schema.String 等节点描述参数,在注册时生成 JSON Schema 并缓存。这个设计不是为了做一套
通用 AST,而是把冗长、易错的 SDK 参数结构压缩成项目内部可校验的声明,同时让重复工具定义
保持一致。
这次重构形成了两个延续至今的原则。第一,外部依赖必须停留在适配层,核心流程只传递项目自己的 结构体和接口;第二,重复且容易写错的协议结构应在边界处集中构造,而不是散落在每个调用方。 代价是项目内部多了一组看似重复的类型和转换代码,但它换来了依赖隔离、稳定测试和 provider 替换能力。与后续几次大规模调整相比,这次重构范围不大,却建立了项目最早的一条稳定边界。
第二次项目重构¶
第二次重构伴随着功能快速增长。项目加入 session registry、上下文压缩、model registry、skill、 memory、CLI 命令路由、Telegram channel、请求级事件流、安全工具执行,以及最初的五阶段建模流程。 如果继续把这些能力都塞进 Agent 实例,Agent 就会同时负责模型调用、会话持久化、命令解析、 权限审批和进程生命周期。因此这次重构的重点不再是某个功能,而是重新分配状态所有权。
最终形成的主调用链可以概括为:
Runtime 管理进程生命周期、context、I/O 和资源关闭;Channel 把 CLI 或 Telegram 输入转换为
统一请求,并消费事件与回复;Application 负责命令路由、session、model、skill、memory 和 modeling
等能力的组合;Agent 则收缩为围绕一次请求执行模型与工具循环。Telegram 后来不再是产品重点,
但它促使我提前把“传输方式”和“业务执行”分开,这条 transport-neutral 边界仍然有价值。
这次重构中逐渐稳定下来的不只是包结构,还有几条运行时不变量:
- 同一 session 的历史、模型选择、prompt snapshot 和 token usage 由 session 统一管理;
- 模型发起的文件写入和 shell 执行必须经过
security.Executor,依次完成策略判断、人工审批和审计; - skill 保存较长的操作流程,memory 保存经过筛选的事实和经验,两者不能未经区分地注入上下文;
runstream只传递有界的进度事件,CLI 和其他 channel 各自负责渲染;- modeling project 的状态推进只能由 Pipeline 完成,阶段实现只返回草稿与证据。
这些约束使项目从“能调用工具的聊天循环”变成了一个具备状态、安全和恢复语义的中型 Go 项目。 例如工具不是简单执行一条 bash:路径先被限制在 workspace 内,危险调用需要交互批准,执行前后 写入审计记录;modeling 也不是连续调用五次模型,而是以 project revision、阶段状态和 artifact 引用保存每一步结果。
但第二次重构也留下了最重要的结构性问题:通用 Agent 和 modeling 是两条平行主线。普通对话会经过
session、memory 和工具循环,/modeling 则由命令层直接驱动独立 Pipeline。建模代码在整个项目中
占据很大比重,却没有成为产品入口和架构中心。两套能力都能单独工作,但组合方式更像并置而不是统一。
这是我第一次独立处理这种规模的架构演进,进度并不快。真正困难的地方也不是“多写几个 interface”, 而是判断哪个对象应该拥有状态、哪个边界必须阻断副作用、哪些抽象只是暂时的适配。到这个阶段我才 真正理解,抽象不是预先画出一张漂亮的分层图,而是在功能反复挤压现有结构后,把已经出现的稳定规则 写进接口、事务和测试中。第二次重构建立了可工作的工程基础,也清楚地暴露了下一次重构必须解决的问题。
第三次项目重构¶
第三次重构仍在进行,它要解决第二次重构留下的核心问题:让 Modeling 成为产品中心,同时继续复用 Agent Runtime 已有的 session、memory、skill、流式输出和安全工具执行能力。这里并不是让 Agent loop 接管 Project 状态机,也不是删除通用运行时后重写一套建模 Agent。更合理的关系是:自然语言和确定性 命令都通过同一组建模用例进入系统;需要推进正式阶段时,仍由 Pipeline 独占 Project 状态迁移, Agent 负责理解意图、调用受控能力并解释结果。
为了隔离产品入口与 Pipeline 实现,我先定义了两层不同稳定性的接口。modelingapi.Service 使用产品语言,
向 CLI、Agent Tool 和未来的 MCP 入口提供 Create、Show、Advance、Apply、Evidence 等用例;请求中的业务参数
与可信的 workspace、user、request 和 idempotency 上下文被有意分开。pipelineapi.Engine 则只使用执行引擎
语言,通过 Describe、Inspect 和 Execute 暴露可发现的 operation。它不知道命令格式、session 历史或 MCP
协议,也不把当前五阶段的 modeling.Stage 直接泄漏给外部调用者。
两层 API 看起来都包含“执行一次建模操作”,但不能简单合并。前者需要长期稳定地承担授权、幂等、审批、 公开错误和结果投影;后者需要允许当前五阶段 Pipeline 在未来被新实现替换。连接两者的应用编排层负责把 可信调用上下文转成 scope,选择 operation,注入运行时能力,并把内部错误和结果映射为公开结构。这样, 将来调整 Stage、Reg-IR 或 Artifact 内部格式时,不必同时修改 CLI、Agent Tool 与 MCP。
目前代码已经建立 modelingapi 与 pipelineapi 的请求、结果、错误和事件契约,也实现了
adapter/pipeline/current:它把 operation 映射到现有 plan、extract、infer、emit、verify 五个阶段,
并将当前 Project、Artifact 和阶段事件投影为新的引擎结构。pipelineapi.RuntimePorts 还把 Repository、
Completion、Effect 和 Event 明确为一次执行所需的环境能力。这里的 Effect 最终仍要落到
security.Executor,因此更换 Pipeline 不等于绕过路径限制、人工审批和审计。
这组结构更接近应用端口与适配器,而不是 Web 意义上的“前端和后端”:它们仍在同一进程中调用,没有
网络协议;结构体转换的目的,是阻止产品 DTO、Pipeline 协议和当前 modeling.Project 共享同一套演进
周期。这一点与最早隔离 OpenAI SDK 的思路相似,但职责更强:LLM adapter 主要隔离第三方类型,当前
Pipeline adapter 还要保护领域状态、授权范围和事务语义不向错误的层级泄漏。
这一轮尚不能宣称完成。当前 Pipeline adapter 与 Runtime Port 已有初稿,但负责授权、幂等和两层转换的 应用服务仍需收口,CLI、Agent Tool 与 MCP 也还没有全部切换到统一入口;Runtime adapter 的 scope、CAS、 caller、错误分类和等价测试同样需要补齐。只有新旧调用路径产生相同状态迁移、Artifact digest、Apply/Verify 结果与公开错误,才能证明这次重构只改变了依赖方向,没有悄悄改变建模行为。相比一次性推翻现有 Pipeline, 我更希望先固定这条可验证的边界,再在边界之后演进建模流程。
后续的规划¶
后续工作首先不是继续增加入口,而是完成第三次重构的等价性验证。我需要补齐应用编排层和 Runtime
adapter,让 CLI 与 Agent Tool 都只依赖 modelingapi.Service,并用契约测试确认新旧路径得到相同的
Project revision、Artifact digest、错误分类以及 Apply/Verify 结果。只有这条边界稳定之后,MCP 才适合作为
同一 Service 的新 transport;否则过早暴露协议,只会把仍在变化的内部 Stage 和数据结构变成外部兼容负担。
独立 CLI 也会继续保留,因为人工查看 diff、批准文件写入和读取构建证据仍然是当前安全闭环的一部分。
第二步是提高输入资料的可信度与可追溯性。当前 extract 主要接受从 PDF 预先转换出的文本,这对大型 TRM 中的跨页表格、脚注和位域关系并不可靠。我计划先建立参考资料注册表,为 TRM、头文件、Linux 驱动、相邻 QEMU 实现和网络资料记录来源等级、路径或 URL、摘要与 hash,再按硬件章节和寄存器表切分内容。Reg-IR 中的 字段和推断也应能反向指向具体证据;资料不足或冲突时进入待确认状态,而不是让模型自动补全。
第三步才是重构 Pipeline 和 Reg-IR。当前流程实际是 plan、extract、infer、emit、verify,emit 与 apply
之间保留人工审批边界。现有 Reg-IR 能描述寄存器、字段、访问权限和部分副作用,但复杂设备还需要版本化的
能力扩展。我会先以自己更熟悉的 SysBus 外设做垂直切片,归纳 MMIO region、IRQ、reset、migration、trace、
构建接入和 qtest 等能力契约。不过,这不意味着强制每个模型都出现同名的 read、write 或
update_irq 函数;函数形状应由设备语义和 QEMU API 决定,工具真正应该验证的是必要行为是否有证据、
生成结果能否构建,以及 guest 可观察状态是否通过 qtest。
最后,在单外设链路能够稳定复现 K230 RMU、STM32F103 GPIO 这类真实任务后,再考虑板级建模。整板并不是 简单生成更多 C 文件,它还涉及地址空间、IRQ 路由、clock/reset 依赖、SoC 容器、machine 接线和启动验证。 更稳妥的起点,是先定义可人工填写且可机械校验的 Board-IR,或先组合已经验证过的 Device Project;如果 单外设产物仍需要大量人工修正,过早扩展到一块板只会放大错误。这个顺序也让我能用每一步的构建、qtest 和上游 review 结果校正设计,而不是只用功能列表衡量项目进度。
总结¶
训练营项目阶段结束时,我完成的并不是一个可以无人值守生成任意外设的成熟 Agent,而是两类相互促进的 工作:一边通过 K230 RMU 和 STM32F103 GPIO 学习如何从硬件资料走到 QEMU 模型、qtest 和上游 review; 另一边把这些步骤整理成一套能够运行、保存中间产物并控制副作用的实验性建模工具链。它目前仍明显依赖 模型能力和人工审查,Pipeline 的抽象边界也还在重构,因此不能用“已经实现自动建模”掩盖尚未解决的问题。
这几个月对我影响最大的,是判断工程质量的尺度发生了变化。一个模型能够生成代码,不代表它理解了硬件; 一个设备能够编译,也不代表 guest 可观察行为正确;一组接口看起来整齐,也不代表状态、权限和失败语义 已经被正确分配。资料来源、状态不变量、受控副作用、可复现测试和上游反馈,才共同决定产物是否可信。
接下来最实际的里程碑,是完成第三次重构并用同一个真实外设任务贯穿新入口:从带来源的资料提取 Reg-IR, 生成并人工审查 diff,经批准落入临时 QEMU 工作树,最后留下构建与 qtest 证据。等这条路径能够稳定复现, 再扩展 MCP、更多 SysBus 类型和板级建模。对我而言,这篇博客记录的不是一个项目的终点,而是我开始把 建模知识、软件架构和社区协作放进同一套工程方法中的阶段节点。