如意Agent六边形架构改造(一):从单体巨石到端口适配器

我是张大鹏,做了十多年人工智能,带过不少项目。说实话,最难的不是让 AI 变聪明,是让承载 AI 的代码能持续维护。上个月我重构了日志系统,这次我要动更大的手术:把项目从一个平铺的单体结构,改造成六边形架构。


一、一个让我夜不能寐的文件夹

先说一个场景。

上周学生反馈了一个 bug:某个 Agent 任务执行到一半卡住了。我按经验去排查------llmcore.py 里有什么问题?打开文件,1568 行。往下翻,ga.py,784 行。再往下,main.py,2635 行。

这不是代码,这是三座大山

三个文件加起来近 5000 行,而且互相引用:

复制代码
agentmain.py ←→ ga.py ←→ agent_loop.py ←→ llmcore.py
                      ↕
                    main.py

想改一个文件?你得先看懂另外四个。想加一个新功能?你得知道在哪一层加。想换一个 LLM 提供商?------llmcore.py 里 Claude、OpenAI、NativeClaude 全部写在一起,牵一发而动全身。

更让人头疼的是,连工具函数都混在一起:

  • ga.py ------ 既管文件读写,又管浏览器控制,还管代码执行,中间还夹着 Agent 的工具调度逻辑
  • llmcore.py ------ API 请求、SSE 流解析、消息格式转换、会话管理,全在一个文件里
  • agent_loop.py ------ Agent 执行循环、BaseHandler、StepOutcome,好的坏的都在一起

这不是这个项目独有的问题,这是所有"从原型长起来"的项目都逃不过的宿命------先让它跑起来,再考虑让它健康。问题在于,项目已经跑到第二年了,"让它健康"这件事不能再拖了。


二、我选择了一个"激进但安全"的方案

在动手之前,我对比了几种架构方案:

方案 核心理念 工作量 风险 长期价值
渐进模块化 拆巨石文件,不动结构 2-3天
分层架构 三层划分:展示/应用/领域 1-2周
六边形架构 端口-适配器,核心与技术隔离 3-4周 最高

我的直觉想选方案一------风险最低,改起来最快。但我问了自己一个问题:"一年后回头看,我希望当初选了哪个方案?" 答案是六边形架构。

理由很简单:如意Agent 需要同时支持多个 LLM 提供商、多个前端界面(桌面/GUI/CLI)、多种记忆存储方式。六边形架构正好解决这个问题------把核心业务逻辑放在最中间,周围用"端口"定义接口,用"适配器"对接具体技术实现。

但风险确实高。所以我选了一套"绞杀藤(Strangler Fig)"策略:

不推倒重来,不中断服务。每改一块,老的那块就变成薄薄的一层转发,新的代码在新位置健康生长。

这套策略有四条铁律:

规则 内容 为什么
每阶段可独立合并 不存在"改到一半跑不起来"的状态 降低风险,随时可以停下来
每阶段测试通过 pytest tests/ 是硬门禁 防止"你以为改好了"的幻觉
旧文件变薄转发 from ga import X → 仍然可用,但 X 在 src/ 向后兼容,逐步替代
端口先行 先定义接口,再实现适配器 确保边界清晰,不改核心逻辑

三、第一阶段:从"最小单元"开刀

按照架构计划,第一阶段的目标是------提取核心数据模型

为什么先从数据模型开始?因为它们是依赖图的"叶子节点"------不依赖任何东西,所有人都依赖它们。

我选了三个模型作为突破口:

模型 原来在哪 新位置
StepOutcome agent_loop.py src/core/model/step_outcome.py
MockResponse llmcore.py src/core/model/llm_response.py
MockToolCall / MockFunction llmcore.py src/core/model/llm_response.py

这三个模型有一个共同特点:纯数据容器StepOutcome 是一个 dataclass,MockResponse 是一个简单的响应包装器。它们不调用 API、不读写文件、不涉及任何外部系统------提取它们基本上就是把代码从老地方"搬"到新地方,不改变任何行为。

TDD 流程

这次改造我严格遵循了 TDD(测试驱动开发)

第一步:写测试,确认失败(🔴 Red)

我先为 StepOutcome 写了 9 个测试:

python 复制代码
# tests/core/unit/test_step_outcome.py
class TestStepOutcome:
    def test_is_dataclass(self):
        assert dataclasses.is_dataclass(StepOutcome)

    def test_creation_with_data_only(self):
        outcome = StepOutcome(data="hello")
        assert outcome.data == "hello"
        assert outcome.next_prompt is None
        assert outcome.should_exit is False

    def test_creation_with_all_fields(self):
        outcome = StepOutcome(data={"key": "value"}, next_prompt="continue", should_exit=True)
        assert outcome.data == {"key": "value"}
        assert outcome.next_prompt == "continue"
        assert outcome.should_exit is True
    # ... 更多边缘情况测试

运行结果:ModuleNotFoundError------模型文件还不存在。这正是我想要的,测试正确地失败了。

第二步:实现模型,让测试通过(🟢 Green)

核心模型代码非常简洁:

python 复制代码
# src/core/model/step_outcome.py
from __future__ import annotations
from dataclasses import dataclass
from typing import Any

@dataclass
class StepOutcome:
    """Result of a single agent execution step."""
    data: Any
    next_prompt: str | None = None
    should_exit: bool = False
python 复制代码
# src/core/model/llm_response.py
class MockResponse:
    def __init__(self, thinking, content, tool_calls, raw, stop_reason="end_turn"):
        self.thinking = thinking
        self.content = content
        self.tool_calls = tool_calls
        self.raw = raw
        self.stop_reason = "tool_use" if tool_calls else stop_reason

运行新测试:26 passed,0 failed

第三步:加向后兼容,重构老文件(🔁 Refactor)

最微妙的一步------让旧代码继续可用。做法是在老文件里加一个 import 转发:

python 复制代码
# agent_loop.py --- 删掉了 StepOutcome 类定义,替换为:
from src.core.model.step_outcome import StepOutcome  # re-export

# llmcore.py --- 删掉了三个 Mock 类定义,替换为:
from src.core.model.llm_response import MockFunction, MockToolCall, MockResponse  # re-export

这样做的结果是:

  • from agent_loop import StepOutcome仍然可用
  • from llmcore import MockResponse仍然可用
  • from src.core.model import StepOutcome新的标准路径

两条路径指向同一个类,不存在两份代码的同步问题。


四、成果量化

改造完成后,我跑了全量验证:

指标 改造前 改造后 变化
测试总数 230 pass / 7 fail 256 pass / 7 fail +26 新测试,0 回归
ruff 新代码 N/A All checks passed
旧导入路径 正常 正常(验证类身份一致)
新建文件 0 8 个 包结构就位
修改文件 0 3 个 老文件变薄转发层

目录结构的变化:

复制代码
GenericAgent/
├── src/                          ← ★ 新增:源码目录
│   └── core/
│       └── model/
│           ├── __init__.py
│           ├── step_outcome.py   ← StepOutcome dataclass
│           └── llm_response.py   ← MockResponse / MockToolCall / MockFunction
├── agent_loop.py                 ← 转发层(-7 行)
├── llmcore.py                    ← 转发层(-22 行)
└── tests/core/unit/              ← ★ 新增:领域模型测试
    ├── test_step_outcome.py      ← 9 个测试
    └── test_llm_response.py      ← 17 个测试

七个失败的测试是已有的时区问题,和我的改动无关。


五、下一个里程碑

第一阶段只是开始。我接下来的计划是这样的:

阶段 内容 预计工时 状态
① 核心数据模型 StepOutcome / MockResponse 提取 0.5d 已完成
② 文件系统和代码执行适配器 code_run / file_* → 适配器 2d
③ LLM 适配器 llmcore.py 拆分为模块化 Session 3-4d
④ 浏览器和记忆适配器 web_scan / memory 提取 2d
⑤ Agent Service GenericAgentHandler 注入化 3-4d
⑥ 装配层 + DI Container,配置管理 2d
⑦ CLI + 桌面适配器 main.py / agentmain.py 变薄 2-3d
⑧ 清理收尾 移除转发层 1d

整个计划大约 16-19 天,每阶段都可独立合并和验证。


总结

维度 内容
核心思路 六边形架构 + Strangler Fig 绞杀藤迁移策略,每阶段可逆可验证
第一阶段 提取 StepOutcome / MockResponse 等核心数据模型到 src/core/model/
开发方式 TDD(Red → Green → Refactor),26 个新测试全覆盖
向后兼容 老文件变薄转发层,所有现有导入路径不受影响
迁移铁律 不推倒重来、每阶段测试通过、每阶段可独立合并

参考资料


作者 :张大鹏
日期:2026-05-03

相关推荐
twc8292 小时前
全链路压测的环境复杂性:网络架构、应用架构与性能影响因素全解析
网络·软件测试·架构·性能测试·全链路压测
yoyo_zzm3 小时前
Laravel10.x新特性全解析
数据库·mysql·架构
冯诺依曼的锦鲤3 小时前
从零实现高并发内存池:TCMalloc 核心架构拆解
c++·学习·算法·架构
nvd113 小时前
深度解析:Kong Hybrid 模式与 KIC (Gateway API) 架构演进与核心异同
架构·gateway·kong
独隅3 小时前
倒排索引与实时检索架构揭秘
架构
你的保护色4 小时前
光纤到户常用架构介绍(无源光网络PON,有源光网络AON)
网络·架构
独隅5 小时前
搜索引擎核心技术栈逆向解析
架构
EXnf1SbYK5 小时前
Redis分布式锁进阶第十二篇:全系列终极兜底复盘 + 锁架构巡检落地 + 线上零事故收尾方案
redis·分布式·架构
0点51 胜5 小时前
[MediaForge] 进阶架构师:从插件化到微内核与沙盒架构深度解析
架构