<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title>首页 · Tommy Cheese</title>
    <link>https://tommycheese.github.io/</link>
    <description>Tommy Cheese 的个人博客，记录软件工程、人工智能与学习实践。</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>zh-CN</language>
    <lastBuildDate>Sun, 13 Sep 2026 00:00:00 +0800</lastBuildDate>
    <atom:link href="https://tommycheese.github.io/index.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>OpenCode v2 扩展机制研究与接入说明</title>
      <link>https://tommycheese.github.io/blogs/opencode-v2-extensions/</link>
      <pubDate>Sun, 13 Sep 2026 00:00:00 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/opencode-v2-extensions/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;研究日期：2026-09-13。本文描述 OpenCode v2 的通用插件能力，包括插件类型、加载机制、能力注册、执行钩子、权限边界与生命周期，供插件开发和嵌入式集成参考。&lt;/p&gt;
&lt;p&gt;代码基线为所研究的 OpenCode 仓库 &lt;code&gt;v2&lt;/code&gt; 分支，提交为 &lt;code&gt;2308db16387c9b59e88d732a93ec9bac54462b03&lt;/code&gt;。源码引用采用该固定提交内的仓库相对路径；文中的接口与行为以此版本为准，不代表其他 OpenCode 版本或旧版插件 API 的兼容承诺。&lt;/p&gt;
&lt;p&gt;本文是源码研究与能力说明。代码示例用于解释接口和调用方式，未经独立运行验证。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="plugin-扩展入口"&gt;Plugin：扩展入口&lt;/h2&gt;
&lt;h3 id="什么是-plugin"&gt;什么是 Plugin&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;OpenCode 的扩展通过注册能力和执行钩子接入已有运行流程。&lt;/strong&gt; Extension 在当前 OpenCode v2 实现中主要对应 Plugin 机制。插件可以提供助手、工具、技能、模型、命令等能力，也可以在指定执行节点调整行为；会话运行、模型请求、工具结果和执行记录仍由 OpenCode 核心处理。&lt;/p&gt;
&lt;figure class="plugin-flow"&gt;&lt;img src="https://tommycheese.github.io/blogimages/opencode-v2-plugin-flow.svg" alt="插件加载后注册能力，会话准备上下文，经请求钩子调用模型；工具调用经执行钩子保存结果并返回模型，最终回答结束本次执行。" width="429" height="722"&gt;&lt;figcaption&gt;Plugin 从加载、能力注册到会话执行的流程&lt;/figcaption&gt;&lt;/figure&gt;
&lt;details&gt;&lt;summary&gt;查看 Mermaid 流程图源码&lt;/summary&gt;&lt;pre&gt;&lt;code class="language-mermaid"&gt;flowchart TD
    A[配置文件、本地插件或 SDK 注册] --&amp;gt; B[插件加载与生命周期管理]
    B --&amp;gt; C[构建助手、工具、技能等能力]
    C --&amp;gt; D[会话选择助手]
    D --&amp;gt; E[准备上下文与工具快照]
    E --&amp;gt; F[请求阶段钩子]
    F --&amp;gt; G[调用模型]
    G --&amp;gt; H{返回内容}
    H --&amp;gt;|工具调用| I[工具执行与钩子]
    I --&amp;gt; J[保存结果与执行事件]
    J --&amp;gt; G
    H --&amp;gt;|最终回答| K[本次执行结束]
&lt;/code&gt;&lt;/pre&gt;&lt;/details&gt;
&lt;h3 id="plugin-的分类"&gt;Plugin 的分类&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;内部插件、外部插件和 SDK 插件按接入来源区分，最终进入同一套运行管理。&lt;/strong&gt; 三者都可以注册助手、工具和执行钩子。区别主要在于谁提供插件定义、如何装入宿主，以及宿主是否额外注入核心内部服务。这里讨论的是服务端插件；终端界面的 TUI 插件属于另一个扩展入口。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th scope="col"&gt;比较项&lt;/th&gt;
&lt;th scope="col"&gt;内部插件&lt;/th&gt;
&lt;th scope="col"&gt;外部插件&lt;/th&gt;
&lt;th scope="col"&gt;SDK 插件&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;提供者&lt;/td&gt;
&lt;td&gt;OpenCode 源码与发行版本&lt;/td&gt;
&lt;td&gt;项目或独立插件包的维护者&lt;/td&gt;
&lt;td&gt;嵌入 OpenCode 的应用宿主&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;入口&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PluginInternal&lt;/code&gt; 的 &lt;code&gt;pre&lt;/code&gt; / &lt;code&gt;post&lt;/code&gt; 集合&lt;/td&gt;
&lt;td&gt;目录发现、配置中的文件或包&lt;/td&gt;
&lt;td&gt;嵌入式宿主的 &lt;code&gt;opencode.plugin(definition)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;装载形式&lt;/td&gt;
&lt;td&gt;核心代码直接导入定义&lt;/td&gt;
&lt;td&gt;解析模块并读取默认导出&lt;/td&gt;
&lt;td&gt;在内存中直接传入插件对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可用接口&lt;/td&gt;
&lt;td&gt;公开 Context，加上宿主注入的内部服务&lt;/td&gt;
&lt;td&gt;公开 Plugin Context&lt;/td&gt;
&lt;td&gt;公开 Plugin Context；可通过闭包使用宿主主动传入的依赖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主要配置方式&lt;/td&gt;
&lt;td&gt;原生配置、内部服务或内置默认值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;plugins[].options&lt;/code&gt; → &lt;code&gt;ctx.options&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;宿主代码与闭包；注册接口没有单独的 &lt;code&gt;options&lt;/code&gt; 参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;更新来源&lt;/td&gt;
&lt;td&gt;跟随 OpenCode 代码版本；部分插件自行监听配置变化&lt;/td&gt;
&lt;td&gt;配置变化、受支持的本地入口变化或包配置变更&lt;/td&gt;
&lt;td&gt;宿主再次注册插件，递增内部修订号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实例范围&lt;/td&gt;
&lt;td&gt;每个 Location 独立激活&lt;/td&gt;
&lt;td&gt;按各 Location 的配置分别激活&lt;/td&gt;
&lt;td&gt;同一宿主共享定义，每个 Location 独立激活&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;适用场景&lt;/td&gt;
&lt;td&gt;实现原生助手、工具、供应商与配置处理&lt;/td&gt;
&lt;td&gt;给已有 OpenCode 服务增加项目或业务能力&lt;/td&gt;
&lt;td&gt;在自己的 JS/TS 应用中嵌入并管理 OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Effect 和 Promise 是插件的两种编写接口方法。一个采用公开接口的 Effect 插件对象，既可以默认导出后由配置加载，也可以直接交给嵌入式 SDK 注册；核心能力可以复用，但加载顺序和配置传入方式会发生变化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内部插件把 OpenCode 自身的一部分功能组织成插件。&lt;/strong&gt; 它们不是用户安装到目录里的特殊文件，而是由核心源码静态导入，并在 &lt;code&gt;PluginInternal&lt;/code&gt; 中明确列入集合。&lt;code&gt;PluginInternal.list()&lt;/code&gt; 获取当前 Location 所需的服务，通过 &lt;code&gt;Effect.provide(context)&lt;/code&gt; 注入内部插件，再交给统一加载器。&lt;/p&gt;
&lt;p&gt;这里有两层 Context：&lt;code&gt;effect(ctx)&lt;/code&gt; 的参数仍然是公开 Plugin Context；内部插件通过 Effect 环境另外取得 &lt;code&gt;Config.Service&lt;/code&gt;、&lt;code&gt;Permission.Service&lt;/code&gt;、&lt;code&gt;Shell.Service&lt;/code&gt;、&lt;code&gt;Location.Service&lt;/code&gt; 等服务。因此，“内部插件有更多能力”的依据是宿主明确注入了依赖，不是插件 ID 以 &lt;code&gt;opencode.&lt;/code&gt; 开头。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th scope="col"&gt;内部插件示例&lt;/th&gt;
&lt;th scope="col"&gt;所处集合&lt;/th&gt;
&lt;th scope="col"&gt;实际职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode.agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pre&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;注册原生助手的基础定义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode.tool.shell&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pre&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;注册 Shell 工具，结合原生执行、权限和运行状态服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;原生供应商、搜索及其他工具插件&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pre&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;提供模型、搜索和工具的基础能力&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode.config.agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;post&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读取助手配置和相关 Markdown 文件，把结果应用到助手清单&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;配置供应商、技能、策略及模型变体插件&lt;/td&gt;
&lt;td&gt;&lt;code&gt;post&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在前面贡献的能力上应用配置或后续处理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;这也解释了自定义助手与内部插件的关系：用户编辑助手配置，真正负责读取、监听和应用这些配置的是 &lt;code&gt;opencode.config.agent&lt;/code&gt;。每个自定义助手是一份 Agent 定义，并不需要对应一个独立插件。外部插件或 SDK 插件也可以注册 Agent，之后再由配置插件调整。&lt;/p&gt;
&lt;p&gt;内置插件同样参与配置中的启停选择。例如 &lt;code&gt;-opencode.config.agent&lt;/code&gt; 会让负责应用助手配置的插件退出当前激活集合，相应配置能力也会受到影响。是否保留某个内置插件，应根据其实际职责判断。&lt;/p&gt;
&lt;p&gt;需要增加依赖 Core 私有服务的原生功能时，通常要修改 OpenCode 源码，把插件加入内置集合；若新增服务依赖，还需要调整相应服务装配。这类变更随 OpenCode 一起构建、发布和升级。内部插件适合引擎自身功能，普通业务工具优先通过公开接口接入。&lt;/p&gt;
&lt;p&gt;依据：内部插件集合与服务注入（&lt;code&gt;packages/core/src/plugin/internal.ts&lt;/code&gt;）、原生助手插件（&lt;code&gt;packages/core/src/plugin/agent.ts&lt;/code&gt;）、助手配置插件（&lt;code&gt;packages/core/src/config/plugin/agent.ts&lt;/code&gt;）、Shell 工具插件（&lt;code&gt;packages/core/src/tool/plugin/shell.ts&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;外部插件通过文件或包给现有 OpenCode 服务增加能力。&lt;/strong&gt; “外部”指代码来源不在内置集合中，执行时仍然装入 OpenCode 进程。加载器向它提供公开 Plugin Context，不会像内部插件一样额外注入 Core 服务。它仍能使用运行环境允许的文件、网络等能力；这不是一个独立进程或安全沙箱。&lt;/p&gt;
&lt;p&gt;外部插件有两条发现路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自动发现&lt;/strong&gt;：扫描原生配置目录下的 &lt;code&gt;plugin/&lt;/code&gt; 和 &lt;code&gt;plugins/&lt;/code&gt;。当前实现直接识别 &lt;code&gt;.ts&lt;/code&gt;、&lt;code&gt;.js&lt;/code&gt; 文件，也支持解析包目录及符合条件的符号链接。包目录会尝试 &lt;code&gt;package.json&lt;/code&gt; 中字符串形式的 &lt;code&gt;exports&lt;/code&gt;、&lt;code&gt;module&lt;/code&gt;、&lt;code&gt;main&lt;/code&gt;，再尝试 &lt;code&gt;index.ts&lt;/code&gt;、&lt;code&gt;index.js&lt;/code&gt;；不能把这里的简单发现逻辑等同于支持所有复杂包导出规则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显式配置&lt;/strong&gt;：&lt;code&gt;plugins&lt;/code&gt; 接受相对路径、绝对路径、文件 URL 和可解析的包。相对路径按配置文件所在目录解析；本地路径进入模块加载，包目标交给包解析器，并尝试 &lt;code&gt;server&lt;/code&gt; 子入口或包根入口。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自动发现结果先进入操作列表，显式配置随后应用，因此可以在配置中关闭自动发现的插件。自动发现并不直接枚举 &lt;code&gt;.mjs&lt;/code&gt; 文件；使用此类入口时，需要显式配置其路径，并由运行环境完成模块加载。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
  "plugins": [
    {
      "package": "./plugins/example.ts",
      "options": {
        "serviceUrl": "https://api.example.com"
      }
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述路径是配置格式示例。插件通过 &lt;code&gt;ctx.options&lt;/code&gt; 读取 &lt;code&gt;serviceUrl&lt;/code&gt;，并自行检查必填项、取值范围和地址约束；传入对象不代表业务配置已经完成校验。&lt;/p&gt;
&lt;p&gt;外部模块必须默认导出一个带有 &lt;code&gt;id + effect&lt;/code&gt; 或 &lt;code&gt;id + setup&lt;/code&gt; 的插件对象：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th scope="col"&gt;编写方式&lt;/th&gt;
&lt;th scope="col"&gt;初始化入口&lt;/th&gt;
&lt;th scope="col"&gt;接入方式与清理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Effect&lt;/td&gt;
&lt;td&gt;&lt;code&gt;effect(ctx)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;由宿主在插件 Scope 内执行；资源绑定 scoped 生命周期或 finalizer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Promise&lt;/td&gt;
&lt;td&gt;&lt;code&gt;setup(ctx)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;加载器通过 &lt;code&gt;fromPromise&lt;/code&gt; 转为 Effect 插件；可以返回 cleanup 函数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code&gt;@opencode-ai/plugin/effect&lt;/code&gt; 提供 Effect API，包根入口 &lt;code&gt;@opencode-ai/plugin&lt;/code&gt; 提供当前分支的 Promise API。旧版插件 API 有独立的 &lt;code&gt;v1&lt;/code&gt; 入口，不能仅凭“同样是 OpenCode 插件”假定接口兼容。&lt;/p&gt;
&lt;p&gt;外部插件的典型加载链路为：&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;配置目录 / plugins 配置
  → ConfigPluginSource 生成有序操作与本地文件时间戳
  → PluginSupervisor 解析路径或包、导入模块、校验默认导出
  → 适配 Promise 定义，并注入该来源的 options
  → Plugin.Service 创建 Location 内的插件实例
  → 注册助手、工具、钩子及需要清理的资源
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;文件或配置变化可以触发重新生成插件集合；这是发现与监听支持范围内的更新，不是任意依赖文件都能实时刷新。特别是显式配置目录入口时，当前实现不保证目录内部文件变化会触发重载。包版本的升级也应通过明确的部署或配置变更管理。&lt;/p&gt;
&lt;p&gt;模块导入或导出格式校验失败会记录加载警告，该次来源被跳过；进入激活阶段后的初始化失败则由统一插件管理处理。诊断时需要分别确认“文件已被发现”“模块已成功解析”“插件已激活”和“能力已注册”，仅看到配置项不足以证明工具可用。&lt;/p&gt;
&lt;p&gt;依据：来源发现与本地监听（&lt;code&gt;packages/core/src/config/plugin/source.ts&lt;/code&gt;）、模块加载与配置注入（&lt;code&gt;packages/core/src/plugin/supervisor.ts&lt;/code&gt;）、公开包入口（&lt;code&gt;packages/plugin/package.json&lt;/code&gt;）、Promise 适配器（&lt;code&gt;packages/plugin/src/promise/adapter.ts&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SDK 插件由嵌入式宿主直接注册，适合把 OpenCode 作为应用内部的执行引擎。&lt;/strong&gt; 本文的 SDK 特指当前分支的 &lt;code&gt;@opencode-ai/sdk-next&lt;/code&gt; 嵌入式宿主。&lt;code&gt;OpenCode.create()&lt;/code&gt; 在应用进程内创建运行环境和内存中的 HTTP 路由调用链，不需要为这条内部调用链开启网络监听；工具、模型和业务服务仍可发起各自的网络请求。&lt;/p&gt;
&lt;p&gt;宿主通过 &lt;code&gt;opencode.plugin(definition)&lt;/code&gt; 直接提交 Effect 插件对象，省去外部模块发现、包解析和默认导出校验。这不是向一个已经启动的远程 OpenCode 服务上传代码。普通 HTTP 客户端和插件 Context 中的 &lt;code&gt;ctx.plugin.list()&lt;/code&gt; 也不等于这个嵌入式注册入口。&lt;/p&gt;
&lt;p&gt;下例展示嵌入式宿主的插件注册与查询方式，需使用与研究基线匹配的依赖版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ts"&gt;import { AbsolutePath, Location, OpenCode } from "@opencode-ai/sdk-next"
import { Effect } from "effect"

const program = Effect.gen(function* () {
  const opencode = yield* OpenCode.create()

  yield* opencode.plugin({
    id: "example.reviewer",
    effect: (ctx) =&amp;gt;
      ctx.agent.transform((draft) =&amp;gt; {
        draft.update("reviewer", (agent) =&amp;gt; {
          agent.description = "检查代码并给出修改建议"
          agent.system = "分析代码质量，并说明建议的依据。"
          agent.mode = "primary"
        })
      }).pipe(Effect.asVoid),
  })

  const location = Location.Ref.make({
    directory: AbsolutePath.make(process.cwd()),
  })
  return yield* opencode.plugin.list({ location })
})

const result = await Effect.runPromise(program.pipe(Effect.scoped))
console.log(result.data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例中的助手只定义用途和提示词，权限需要另行配置。示例执行结束会关闭拥有宿主的 Scope；真实嵌入式应用应让该 Scope 覆盖整个服务生命周期。&lt;/p&gt;
&lt;p&gt;SDK 插件的范围和更新方式需要分两层理解：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th scope="col"&gt;层次&lt;/th&gt;
&lt;th scope="col"&gt;管理内容&lt;/th&gt;
&lt;th scope="col"&gt;生效范围&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;宿主注册表&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Map&amp;lt;plugin.id, Versioned&amp;gt;&lt;/code&gt;，保存定义和递增修订号&lt;/td&gt;
&lt;td&gt;同一个嵌入式宿主共享；不同宿主拥有各自的注册表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Location 激活集合&lt;/td&gt;
&lt;td&gt;该目录上下文内的插件实例、Transform、Hook 与 Scope&lt;/td&gt;
&lt;td&gt;每个 Location 分别激活与清理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;每次注册都会发布 &lt;code&gt;sdk.plugin.updated&lt;/code&gt;。已启动的 Location 收到更新后重新生成插件集合；之后新启动或被回收后重新启动的 Location，从宿主注册表读取当前定义。因此，“宿主只注册一次”不意味着插件初始化只执行一次。闭包中的可变对象也可能被同一宿主的多个 Location 实例共享，不能默认把它当作单个会话的状态。&lt;/p&gt;
&lt;p&gt;同一 SDK 注册表内再次提交相同 ID，会替换定义并产生新修订号；不同宿主之间互不覆盖。注册返回仅表示定义已写入并发布更新，不代表所有 Location 已同步完成激活。需要确认生效时，应检查目标 Location 的实际插件和能力清单，并在必要时等待激活事件。&lt;/p&gt;
&lt;p&gt;当前 SDK 注册表只提供 &lt;code&gt;register&lt;/code&gt; 与 &lt;code&gt;all&lt;/code&gt;，没有独立的 &lt;code&gt;unregister&lt;/code&gt; 接口。各 Location 的配置仍可以通过插件 ID 禁用其激活，但这不会从宿主注册表删除定义。关闭宿主会清理运行资源；重新创建宿主需要重新注册插件，原来的内存注册不是持久安装记录。&lt;/p&gt;
&lt;p&gt;SDK 注册入口接收 Effect 插件，外部加载器对 Promise 插件的自动适配不会在这里自动发生。需要复用 Promise 定义时，应显式使用对应的 &lt;code&gt;fromPromise&lt;/code&gt; 适配器。SDK 路径没有额外的 &lt;code&gt;options&lt;/code&gt; 注入，公开 Context 默认提供空对象；宿主通常用工厂函数或闭包传入自己的配置与业务客户端。&lt;/p&gt;
&lt;p&gt;SDK 插件也不会自动取得内部插件的 Core 服务环境。宿主可以主动传入依赖，但直接依赖 Core 私有服务意味着承担额外的版本耦合。研究基线中的 &lt;code&gt;sdk-next&lt;/code&gt; 还是 &lt;code&gt;private: true&lt;/code&gt; 的过渡包，不能把这个示例视为已发布稳定 npm API 的安装承诺。&lt;/p&gt;
&lt;p&gt;依据：嵌入式宿主入口（&lt;code&gt;packages/sdk-next/src/opencode.ts&lt;/code&gt;）、SDK 注册表（&lt;code&gt;packages/core/src/plugin/sdk.ts&lt;/code&gt;）、SDK 包状态（&lt;code&gt;packages/sdk-next/package.json&lt;/code&gt;）、公开插件宿主（&lt;code&gt;packages/core/src/plugin/host.ts&lt;/code&gt;）。嵌入式测试源码（&lt;code&gt;packages/sdk-next/test/embedded.test.ts&lt;/code&gt;）包含跨 Location 更新、Location 回收后重新激活及宿主间隔离的用例；本文核对了这些用例，未独立运行测试。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;三种来源会合并成一个有顺序的激活集合。&lt;/strong&gt; 在启用项确定之后，当前顺序是：&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;内部 pre → SDK 注册插件 → 外部文件 / 包插件 → 内部 post
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ConfigPluginSource&lt;/code&gt; 负责外部来源与配置操作；&lt;code&gt;PluginSupervisor&lt;/code&gt; 把它们与内部、SDK 定义合并；&lt;code&gt;Plugin.Service&lt;/code&gt; 负责最终的重复 ID 检查、Scope、初始化与替换。不要把不同来源理解为三套独立运行引擎。&lt;/p&gt;
&lt;p&gt;这个顺序意味着外部和 SDK 插件贡献的助手等能力，仍可能被后置配置插件调整。需要确定助手的最终提示词、模型或权限时，应读取最终能力清单，而不是只看某个插件中的初始定义。&lt;/p&gt;
&lt;p&gt;插件 ID、包名和入口路径承担不同职责。比如配置通过文件加载一个导出 ID 为 &lt;code&gt;example.reviewer&lt;/code&gt; 的插件，禁用时应写 &lt;code&gt;-example.reviewer&lt;/code&gt;。选择器支持精确 ID、&lt;code&gt;prefix.*&lt;/code&gt; 和 &lt;code&gt;*&lt;/code&gt;；配置操作按顺序执行，后续操作可以重新启用已有定义。&lt;/p&gt;
&lt;p&gt;有两个容易混淆的重名规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同一 SDK 注册表内重复注册相同 ID&lt;/strong&gt;：更新该条定义，属于注册表支持的替换操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不同有效来源最终贡献相同 ID&lt;/strong&gt;：激活阶段按重复 ID 拒绝这一轮集合，不是按来源优先级静默覆盖。外部文件与 SDK 注册不要同时提交同一插件定义。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外，当前 &lt;code&gt;Plugin.Service&lt;/code&gt; 在整个有序集合的 ID 与版本完全一致时跳过激活；一旦集合发生变化，会遍历新集合，清理并重新初始化其中已有的插件。因此，一个来源发生更新也可能让其他未修改的插件重新初始化。三类插件都应正确释放资源，避免把初始化当作只执行一次的业务动作。替换失败会尝试恢复该插件旧版本，但无法回滚已产生的外部业务副作用。&lt;/p&gt;
&lt;p&gt;依据：来源合并与启停顺序（&lt;code&gt;packages/core/src/plugin/supervisor.ts&lt;/code&gt;）、统一激活与替换（&lt;code&gt;packages/core/src/plugin.ts&lt;/code&gt;）、配置与加载顺序测试源码（&lt;code&gt;packages/core/test/config/plugin.test.ts&lt;/code&gt;）。&lt;/p&gt;
&lt;h2 id="plugin-核心实现一-transform"&gt;Plugin 核心实现一：Transform&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Transform 用来声明能力，Hook 用来处理一次运行。&lt;/strong&gt; 选择扩展点时，应先确定需求属于哪种领域。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th scope="col"&gt;需求&lt;/th&gt;
&lt;th scope="col"&gt;原生扩展接口&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;注册、修改或移除助手&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.agent.transform&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;注册实际可执行工具&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.tool.transform&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;提供技能说明与资源入口&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.skill.transform&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;增加命令&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.command.transform&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调整供应商、模型目录与默认选择&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.catalog.transform&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;提供参考资料来源&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.reference.transform&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;配置认证和连接方式&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.integration.transform&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;提供搜索后端&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.websearch.transform&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修改本轮上下文与可见工具&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.session.hook("context", ...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;检查工具输入或整理结果&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.tool.hook(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调整模型 SDK 或模型实例&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.aisdk.hook(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调整模型 HTTP 请求或响应&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.session.hook("http.request" / "http.response", ...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调整命令创建参数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.shell.hook("create.before", ...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;观察实时事件&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.event.subscribe()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;创建、投递、等待或中断会话&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctx.session.*&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Agent、Catalog、Command、Integration、Reference、Skill 等状态领域保存活动 Transform，并从基础状态按顺序重建。卸载某个插件后，对应 Transform 被移除，再重新生成结果。Transform 应只编辑本次回调提供的 Draft，不应保存 Draft 引用，也不适合在重建时产生外部业务副作用。需要外部数据时，先加载并保存数据，再触发相应领域的 &lt;code&gt;reload()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Tool 的底层实现采用与 Scope 绑定的工具注册和请求快照。它与上述领域具有相同的生命周期归属，但不能推断所有 &lt;code&gt;transform&lt;/code&gt; 都使用完全相同的状态容器或返回类型。&lt;/p&gt;
&lt;h2 id="plugin-核心实现二-hook"&gt;Plugin 核心实现二：Hook&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Hook（执行钩子）是宿主在运行流程中预留的扩展点。&lt;/strong&gt; 插件向某个扩展点注册回调函数，OpenCode 执行到对应节点时，便自动调用这些回调，并传入该节点的上下文数据。插件可以按接口约定读取数据、调整参数或处理结果，从而参与模型请求、工具执行等流程。&lt;/p&gt;
&lt;p&gt;Hook 的使用分为注册和触发两个阶段：插件初始化时，通过 &lt;code&gt;ctx.session.hook(...)&lt;/code&gt;、&lt;code&gt;ctx.tool.hook(...)&lt;/code&gt; 等接口声明要参与的节点；实际运行到该节点时，宿主才执行注册的回调。注册一次后，回调可以随着对应节点再次执行而多次触发。宿主会等待回调完成，再按该接口的约定继续处理；例如 &lt;code&gt;tool.execute.before&lt;/code&gt; 允许通过 &lt;code&gt;Tool.Error&lt;/code&gt; 拒绝这次工具执行。&lt;/p&gt;
&lt;p&gt;Transform 负责构建可用能力，Hook 负责调整能力在具体执行节点的行为。例如，注册一个工具使用 &lt;code&gt;ctx.tool.transform&lt;/code&gt;；检查某次工具调用的输入，可以使用 &lt;code&gt;ctx.tool.hook("execute.before", ...)&lt;/code&gt;；调整即将发送给模型的上下文，可以使用 &lt;code&gt;ctx.session.hook("context", ...)&lt;/code&gt;。Hook 参与当前调用链，而 &lt;code&gt;ctx.event.subscribe()&lt;/code&gt; 用于订阅事件并观察运行情况。&lt;/p&gt;
&lt;p&gt;运行 Hook 按注册顺序串行执行，后注册的 Hook 可以看到前面的修改。多个插件修改同一字段时，需要检查最终加载顺序；内置插件分为前置与后置集合，不能简单假定“外部插件永远最后覆盖”。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th scope="col"&gt;Hook&lt;/th&gt;
&lt;th scope="col"&gt;可修改或观察的内容&lt;/th&gt;
&lt;th scope="col"&gt;注意事项&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session.context&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;系统提示、消息、本轮工具定义&lt;/td&gt;
&lt;td&gt;助手和模型标识是上下文身份；修改消息仅代表本轮请求调整，不等于追加持久历史&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tool.execute.before&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;工具输入&lt;/td&gt;
&lt;td&gt;可以返回 &lt;code&gt;Tool.Error&lt;/code&gt; 阻止执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tool.execute.after&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;成功结果或工具错误&lt;/td&gt;
&lt;td&gt;用于整理结果和补充信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session.http.request/response&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;模型 HTTP 请求与响应&lt;/td&gt;
&lt;td&gt;可能接触凭据和完整输入，需控制记录内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aisdk.sdk/language&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SDK 与实际模型实例&lt;/td&gt;
&lt;td&gt;适合供应商适配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shell.create.before&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;命令、目录、时限、Shell 与环境变量&lt;/td&gt;
&lt;td&gt;字符串规则本身不能提供系统级隔离&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;当前公开 Hook 类型中，只有 &lt;code&gt;tool.execute.before&lt;/code&gt; 声明了可恢复的 &lt;code&gt;Tool.Error&lt;/code&gt; 失败通道。其他 Hook 不应被当作任意抛出业务错误的通用中间件。&lt;/p&gt;
&lt;p&gt;依据：Plugin Context（&lt;code&gt;packages/plugin/src/effect/plugin.ts&lt;/code&gt;）、状态重建（&lt;code&gt;packages/core/src/state.ts&lt;/code&gt;）、Hook 注册与执行（&lt;code&gt;packages/core/src/plugin/hooks.ts&lt;/code&gt;）、工具注册与快照（&lt;code&gt;packages/core/src/tool.ts&lt;/code&gt;）。&lt;/p&gt;
&lt;h2 id="工具示例与生命周期"&gt;工具示例与生命周期&lt;/h2&gt;
&lt;h3 id="示例一-使用-plugin-transform-定义-tool"&gt;示例一：使用 Plugin Transform 定义 Tool&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;工具包含模型可见的定义和宿主执行的函数。&lt;/strong&gt; 模型生成工具名与输入后，OpenCode 使用本轮捕获的工具能力处理调用。工具函数接收宿主提供的 &lt;code&gt;sessionID&lt;/code&gt;、&lt;code&gt;agent&lt;/code&gt;、&lt;code&gt;messageID&lt;/code&gt; 和调用 &lt;code&gt;id&lt;/code&gt;，长操作可以通过 &lt;code&gt;context.progress()&lt;/code&gt; 提供进度。&lt;/p&gt;
&lt;p&gt;下面是一个使用 Effect API 的独立工具示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ts"&gt;import { Plugin } from "@opencode-ai/plugin/effect"
import { Effect, Schema } from "effect"

export default Plugin.define({
  id: "example.echo",
  effect: Effect.fn(function* (ctx) {
    yield* ctx.tool.transform((tools) =&amp;gt; {
      tools.add({
        name: "echo",
        description: "返回收到的文字",
        input: Schema.Struct({ text: Schema.String }),
        output: Schema.Struct({ text: Schema.String }),
        options: { codemode: false },
        execute: ({ text }) =&amp;gt;
          Effect.succeed({
            output: { text },
            content: text,
          }),
      })
    })
  }),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果字段需要按用途区分：&lt;code&gt;output&lt;/code&gt; 是声明输出 Schema 后供程序使用的结构化值；&lt;code&gt;content&lt;/code&gt; 提供给模型并进入会话内容；&lt;code&gt;metadata&lt;/code&gt; 承载有界的附加状态。未声明输出 Schema 却返回 &lt;code&gt;output&lt;/code&gt;，在当前运行实现中属于错误。&lt;/p&gt;
&lt;p&gt;源码中有两个比接口名称更具体的校验细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tool.execute.before&lt;/code&gt; 先于工具运行函数中的 Schema 解码执行。因此 Hook 读取输入时仍应把它当作未知结构；Hook 修改后的输入才进入后续解码。&lt;/li&gt;
&lt;li&gt;Effect Schema 和支持的 Standard Schema 会执行运行时输入校验；纯 JSON Schema 分支直接传递输入。纯 JSON Schema 输出分支也只检查是否为 JSON 值，不逐项验证声明的约束。业务工具不能把“向模型提供了 JSON Schema”视为“服务端已验证参数”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在当前工具通道里，执行前 Hook 自身失败会直接中止后续流程；不能假定 &lt;code&gt;execute.after&lt;/code&gt; 总会为这类拒绝触发。审计设计需要覆盖拒绝路径，而不是只监听成功执行之后。&lt;/p&gt;
&lt;p&gt;预期、可恢复的工具失败可以映射为 &lt;code&gt;Tool.Error&lt;/code&gt;。取消执行、未知程序缺陷和成功结果需要保留不同语义，不应统一吞掉后返回一条普通成功文本。&lt;/p&gt;
&lt;p&gt;依据：工具类型（&lt;code&gt;packages/schema/src/tool.ts&lt;/code&gt;）、工具执行封装（&lt;code&gt;packages/core/src/tool.ts&lt;/code&gt;）、实际参数校验（&lt;code&gt;packages/core/src/tool/runtime.ts&lt;/code&gt;）。&lt;/p&gt;
&lt;h3 id="示例二-使用-code-mode-组合工具调用"&gt;示例二：使用 Code Mode 组合工具调用&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Code Mode 决定一部分工具如何呈现与组合。&lt;/strong&gt; 当前分支会把 &lt;code&gt;codemode: false&lt;/code&gt; 的工具直接暴露给模型；其他工具可进入 Code Mode 目录，通过 &lt;code&gt;execute&lt;/code&gt; 运行一小段代码来组合调用和处理结果。Code Mode 中的实际工具调用仍回到宿主的工具执行路径。&lt;/p&gt;
&lt;p&gt;这适合连续查询、过滤和聚合等场景。Code Mode 运行环境限制直接文件访问、导入等操作，但这类限制不能等同于整个 OpenCode 服务或外部插件都运行在系统沙箱中。上面的 &lt;code&gt;echo&lt;/code&gt; 示例设置了 &lt;code&gt;codemode: false&lt;/code&gt;，展示工具直接暴露给模型的方式。&lt;/p&gt;
&lt;p&gt;模型请求捕获当时的工具注册快照。插件热更新不会把已经准备好的请求视为使用全新的工具清单；后续请求会重新准备能力。插件贡献的工具执行函数本身也应避免依赖已被清理的无归属后台资源。&lt;/p&gt;
&lt;p&gt;依据：工具分类与快照（&lt;code&gt;packages/core/src/tool.ts&lt;/code&gt;）、Code Mode（&lt;code&gt;packages/core/src/codemode/tool.ts&lt;/code&gt;）、本轮工具可用性检查（&lt;code&gt;packages/core/src/session/model-request.ts&lt;/code&gt;）。&lt;/p&gt;
&lt;h3 id="plugin-的-location"&gt;Plugin 的 Location&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;插件实例按 Location 管理，持久业务状态需要单独保存。&lt;/strong&gt; Location 表示由目录和可选原生 Workspace 标识构成的执行上下文。一个 OpenCode 进程可以同时承载多个 Location，同一插件可能在多个 Location 中分别激活。&lt;/p&gt;
&lt;p&gt;每个插件实例拥有 Scope，用于归属 Transform、Hook、工具注册和正确绑定的资源。Scope 关闭时，这些注册会清理。插件自己的定时器、网络订阅和文件监听也需要绑定清理逻辑；Promise 插件可以从 &lt;code&gt;setup&lt;/code&gt; 返回 cleanup，Effect 插件使用相应的 scoped 资源与 finalizer。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;首次加载 → 创建 Scope → 注册能力与钩子 → 服务会话
文件或配置变化 → 替换插件 → 清理旧 Scope → 激活新版本
新版本激活失败 → 尝试恢复旧版本 → 恢复失败则停用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地插件入口文件和配置来源存在变化监听；显式入口是目录时，目录内部文件修改不具备同样的自动热更新保证。加载、更新和停止还受实际文件监听与运行状态影响，因此“支持热更新”不能替代读取原生能力清单的验证。&lt;/p&gt;
&lt;p&gt;插件恢复只能恢复代码和注册，无法回滚已经操作文件、数据库或远程服务产生的副作用。定时任务、审批状态、重试次数、幂等标识应由可靠存储维护。内存里的会话状态至少按 &lt;code&gt;sessionID&lt;/code&gt; 隔离；模块级全局变量还可能跨插件实例共享，需要格外谨慎。&lt;/p&gt;
&lt;p&gt;依据：插件 Scope 与恢复（&lt;code&gt;packages/core/src/plugin.ts&lt;/code&gt;）、插件文件监听（&lt;code&gt;packages/core/src/config/plugin/source.ts&lt;/code&gt;）。&lt;/p&gt;
&lt;h2 id="plugin-最佳实践"&gt;Plugin 最佳实践&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;插件开发优先复用已有配置与公开接口&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只调整提示词、用途或步骤上限时，使用 Agent 配置；&lt;/li&gt;
&lt;li&gt;提供方法和知识时，使用 Skill；&lt;/li&gt;
&lt;li&gt;增加实际操作时，注册 Tool；&lt;/li&gt;
&lt;li&gt;需要在&lt;strong&gt;执行节点&lt;/strong&gt;调整行为时，使用对应 Hook；&lt;/li&gt;
&lt;li&gt;特别的，插件需要持久化业务状态或调用外部服务时，应显式设计存储、认证、事务与幂等处理，不依赖插件内存状态承担这些保证。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;单一职责插件可以只有一个 TS 扩展文件。只有多个能力需要独立启停、版本或失败策略时，才拆成多个插件 ID。普通外部插件应依赖公开插件、客户端和 schema 接口，避免导入 Core 私有实现。分发时需明确兼容的宿主版本、依赖版本、模块入口和构建方式。&lt;/p&gt;
&lt;p&gt;升级 Plugin 时，应至少检查以下行为，而不是只确认插件模块能够导入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Agent、工具和技能是否进入实际能力清单，卸载后贡献是否消失。&lt;/li&gt;
&lt;li&gt;多插件顺序、配置覆盖和热更新失败时的最终状态是否符合预期。&lt;/li&gt;
&lt;li&gt;工具输入、输出、取消和执行拒绝是否保持正确语义。&lt;/li&gt;
&lt;li&gt;会话模型与助手选择是否真正生效，子助手是否具有预期权限和上下文。&lt;/li&gt;
&lt;li&gt;自定义工具及其调用的服务是否正确检查会话归属、业务授权、状态转换和重复请求。&lt;/li&gt;
&lt;li&gt;服务重启后，持久状态能否恢复，实时事件缺失是否得到正确处理。&lt;/li&gt;
&lt;li&gt;工具结果与事件是否准确表达进度、完成、失败和取消状态。&lt;/li&gt;
&lt;/ul&gt;

          </description>
    <category>Agent 开发</category><category>插件开发</category></item>
    <item>
      <title>再读整洁架构之道（六）边界</title>
      <link>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E5%85%AD/</link>
      <pubDate>Mon, 29 Jul 2024 06:13:15 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E5%85%AD/</guid>
      <description>&lt;h2 id="什么是边界"&gt;什么是边界&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;软件架构设计本身就是一门划分边界的艺术。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;边界的作用是将软件划分为各种元素，以便约束边界两侧的依赖关系。在项目初期划分的边界可以方便我们将一些决策尽量延后进行，确保未来这些决策不会对系统额&lt;strong&gt;核心业务逻辑&lt;/strong&gt;造成干扰。划分边界同时可以降低或消除架构中不必要耦合，系统中存在的耦合——尤其是那些过早做出的、不成熟的决策所导致的耦合（如采用的框架，数据库等元素，这些细节应该被推迟）是系统最消耗人力资源的部分。&lt;/p&gt;
&lt;p&gt;那么如何划分边界呢？可以遵循如下的基本原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;边界应该划分在那些无关的事情中间：如 GUI 与业务逻辑，这意味着输入输出（GUI）对于业务逻辑来说并不重要，业务逻辑可以选择多种不同的 GUI 实现；&lt;/li&gt;
&lt;li&gt;边界线应该沿着系统的变更轴来画。也就是说，位于边界线两侧的组件应该以不同原因、不同速率变化着。指导变更轴心的原则包括模块/类级别的SRP原则和组件级别的CCP原则。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在了解划分边界的重要作用，并且了解如何划分边界后，可以进一步探讨该如何划分边界了，即划分边界的基本流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先将系统分割成组件，其中一部分是系统的核心业务逻辑组件，另一部分是与核心业务无关但负责提供必要功能的插件；&lt;/li&gt;
&lt;li&gt;通过对源代码的修改，让这些非核心组件依赖于系统的核心业务逻辑组件；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;划分边界是一种对依赖反转原则和稳定抽象原则的具体应用，依赖剪头应该由底层具体实现指向高层抽象的方向。&lt;/p&gt;
&lt;h2 id="边界剖析"&gt;边界剖析&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;一个系统的架构是由一系列软件组件以及它们之间的边界共同定义的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;边界具有多种形式，本章以跨边界调用为例来剖析边界。&lt;/p&gt;
&lt;p&gt;在运行时，跨边界调用指的是边界线一侧的函数调用另一侧的函数，并同时传递数据的行为。构造合理的跨边界调用需要我们对源码中的依赖关系进行合理管控，若不对依赖管控，可能导致一个模块的源码发生变更时，其他模块的源码也可能会随之发生变更或重新编译，并需要重新部署，划清边界有助于减少这种情况的发生。&lt;/p&gt;
&lt;p&gt;跨边界调用包含不同的类别：源码层次、部署层次、服务层次以及物理边界。&lt;/p&gt;
&lt;h3 id="源码层次的跨边界调用"&gt;源码层次的跨边界调用&lt;/h3&gt;
&lt;p&gt;源码层次的跨边界调用出现在单体架构中，这类架构一般都需要利用某种动态形式的多态来管理其内部的依赖关系。&lt;/p&gt;
&lt;p&gt;最简单的跨边界调用形式，是由低层客户端来调用高层服务函数，这种依赖关系在运行时和编译时会保持指向一致，都是从低层组件指向高层组件。&lt;/p&gt;
&lt;p&gt;（原示意图暂缺）&lt;/p&gt;
&lt;p&gt;但当高层组件中的客户端需要调用低层组件中的服务时，我们就需要运用动态形式的多态来反转依赖关系（图中的Service接口是某种形式的SPI）。&lt;/p&gt;
&lt;p&gt;（原示意图暂缺）&lt;/p&gt;
&lt;h3 id="部署层次的跨边界调用"&gt;部署层次的跨边界调用&lt;/h3&gt;
&lt;p&gt;部署层次的跨边界调用将其所有可部署的单元打包成一个便于操作的文件格式，除这一点以外，这种按部署层次解耦的组件与单体结构几乎是一样的。&lt;/p&gt;
&lt;p&gt;部署层次的跨边界调用涉及到物理边界，因为不同部署的单元需要跨越物理边界来完成通信。常见的物理边界包括动态链接库、线程、本地进程等。&lt;/p&gt;
&lt;h3 id="服务层次的跨边界调用"&gt;服务层次的跨边界调用&lt;/h3&gt;
&lt;p&gt;服务是系统架构中最强的边界形式，一个服务就是一个进程。在划分架构边界时，一定要尽可能地控制通信次数。在这个层次上通信必须能够适应高延时情况。除此之外，我们可以在服务层次上使用与本地进程相同的规则。也就是让较低层次服务成为较高层次服务的“插件”。&lt;/p&gt;

          </description>
    <category>软件架构</category><category>架构整洁之道</category></item>
    <item>
      <title>再读整洁架构之道（五）软件架构</title>
      <link>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%BA%94/</link>
      <pubDate>Sun, 28 Jul 2024 19:23:23 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%BA%94/</guid>
      <description>&lt;p&gt;是时候进入软件架构了。在之前已经介绍了模块/类、组件的相关内容，这一部分会重点介绍软件架构的基本定义。&lt;/p&gt;
&lt;h2 id="写在最前面"&gt;写在最前面&lt;/h2&gt;
&lt;p&gt;🌟软件架构师自身需要是程序员，并且必须一直坚持做一线程序员，如果不亲身承受因系统设计而带来的麻烦，就体会不到设计不佳所带来的痛苦，接着就会逐渐迷失正确的设计方向。&lt;/p&gt;
&lt;h2 id="什么是软件架构"&gt;什么是软件架构？&lt;/h2&gt;
&lt;p&gt;软件架构工作的实质就是讨论规划如何将系统切分成组件，并安排好组件之间的排列关系与通信方式，即确定边界。&lt;/p&gt;
&lt;p&gt;软件架构设计的目的一般有两个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为了在工作中更好的对这些组件进行研发、部署、运行以及维护；&lt;/li&gt;
&lt;li&gt;如果想设计一个便于推进各项工作的系统，其策略就是要在设计中尽可能长时间地&lt;strong&gt;保留尽可能多的可选项&lt;/strong&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;一个软件系统的架构质量和该系统是否能正常工作（行为）的关系并不大，毕竟世界上有很多架构设计糟糕但是工作正常的软件系统。真正的麻烦往往会出现在这个软件系统的开发、部署以及后续的补充开发中。这也间接反映的作者的观点：软件系统的架构价值在一定程度上大于其行为价值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;软件架构设计的目标如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心目标：一个良好的架构设计应该围绕着用例来展开，良好的架构设计应该只关注用例，并能将它们与其他的周边因素隔离，隔离的方式是尽可能推迟选择，保留更多的可选项；&lt;/li&gt;
&lt;li&gt;主要目标：支撑软件系统的全生命周期，设计良好的架构可以让系统便于理解、易于修改、方便维护，并且能轻松部署；&lt;/li&gt;
&lt;li&gt;终极目标：最大化程序员的生产力（解放生产力），同时最小化系统的&lt;strong&gt;总&lt;/strong&gt;运营成本；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;软件架构的设计重点是将&lt;strong&gt;策略&lt;/strong&gt;彼此分离，然后将它们按照变更的方式进行重新分组，也就是划分出边界，在这一部分可以依据的原则包括SRP、CCP、CSP，SDP、SAP等等。&lt;/p&gt;
&lt;h2 id="软件架构的职责"&gt;软件架构的职责&lt;/h2&gt;
&lt;p&gt;软件架构具有开发、部署、运行以及维护多个方面的职责。&lt;/p&gt;
&lt;p&gt;在开发上：软件架构要方便软件系统的开发，因此不同的团队应该使用不同的软件架构设计。这在一定程度上也体现了康威定律。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么康维定律能够体现团队的架构？看视频了解更多：&lt;a href="https://www.bilibili.com/video/BV1bb421E7i6/?spm_id_from=333.337.search-card.all.click&amp;amp;vd_source=8c0f6bcaf6b2e92f574553f45e565994"&gt;康威定律：为什么你的架构会反映团队结构？_哔哩哔哩_bilibili&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在部署上：软件架构要实现软件的一键式轻松部署；&lt;/p&gt;
&lt;p&gt;在运行上，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正如前面提到的，架构对于软件系统的高效运行远小于其他几个方面，作者认为这主要源于增加硬件资源可以弥补架构在运行方面的考虑不足，而架构设计通常需要更为昂贵的人力资源；&lt;/li&gt;
&lt;li&gt;除了架构设计对于高效运行的作用，架构还应该能反应系统在运行时的需求，也就是说，设计良好的系统架构应该可以使开发人员对系统的运行过程一目了然。架构应该起到揭示系统运行过程的作用，架构应该将系统中的用例、功能以及该系统的必备行为设置为对开发者可见的一级实体，简化它们对于系统的理解，《尖叫的软件架构》一章就是在重点说明这个问题；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在维护上，软件系统维护的成本一般是最高的，维护的成本可以分为&lt;strong&gt;探秘&lt;/strong&gt;与&lt;strong&gt;风险&lt;/strong&gt;两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;探秘：探秘(spelunking)的成本主要来自我们对于现有软件系统的挖掘，目的是确定新增功能或被修复问题的最佳位置和最佳方式；&lt;/li&gt;
&lt;li&gt;风险：风险(risk)，则是指当我们进行上述修改时，总是有可能衍生出新的问题，这种可能性就是风险成本；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="保持可选项"&gt;保持可选项&lt;/h2&gt;
&lt;p&gt;回忆之前提到的软件架构提到的两种价值：“行为价值”和“架构价值”，提升架构价值的方式就是让软件更软，而让软件更软的方式就是让软件架构尽可能多的保持可选项。&lt;/p&gt;
&lt;p&gt;软件系统的元素分为&lt;strong&gt;策略&lt;/strong&gt;与&lt;strong&gt;细节&lt;/strong&gt;两类，策略体现的是软件中所有的业务规则与操作过程，因此它是系统的真正价值所在，细节则是指那些让操作该系统的人、其他系统以及程序员们与策略进行交互，但是又不会影响到策略本身的行为。如 IO 设备、数据库等等，细节选项应该是尽可能保留的。&lt;/p&gt;
&lt;p&gt;软件架构师的目标就是要创建一种系统形态，该形态会以策略为&lt;strong&gt;最基本的元素&lt;/strong&gt;，并让细节与策略脱离关系，以允许在具体决策过程中&lt;strong&gt;推迟或延迟与细节相关的内容&lt;/strong&gt;。因为越到项目的后期，我们就拥有越多的信息来做出合理的决策。一个优秀的软件架构师应该致力于最大化可选项的数量。&lt;/p&gt;
&lt;p&gt;作者使用设备无关性的例子来说明推迟细节的重要性。现代操作系统的输入输出设备种类多样，而在计算机发展的最初时期，打孔纸带是主流（几乎是唯一）的一种输入输出方式，编程人员很自然的就将读取/输出纸带的代码耦合到了系统代码当中，当磁带出现后，开发人员不得不开发新的代码来适配磁带输入输出，光盘出现后…为了应对这种重复开发，适应性差的问题，开发人员提出了设备无关行的概念，即将设备抽象成函数，OS会利用函数与输入输出设备交互，而开发人员只需要提供这些函数的具体实现。此时输入输出就成为了OS的插件，这也是开闭原则的雏形（拥抱👏新增、抗拒🥊修改！）。&lt;/p&gt;
&lt;h2 id="保持独立性"&gt;保持独立性&lt;/h2&gt;
&lt;p&gt;良好的软件架构应该有充足的独立性。&lt;/p&gt;
&lt;p&gt;解耦可以让我们保证软件架构的独立性。&lt;/p&gt;
&lt;p&gt;解耦分为水平解耦与垂直解耦。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;水平解耦（按层解耦）：系统可以被解耦成若干个水平分层——UI、数据库等等；&lt;/li&gt;
&lt;li&gt;用例解耦（垂直解耦）：在水平分层解耦的同时，也按用例将其切分成多个垂直分片，如：将增加订单的用例UI与删除订单的用例UI分开。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果我们按照变更原因的不同对系统进行解耦，就可以持续地向系统内添加新的用例，而不会影响旧有的用例。&lt;/p&gt;
&lt;p&gt;同样，解耦的层次也可以是不同的。可以在源码、部署以及服务三个不同的层次来完成解耦。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源码层次：控制源码中模块之间的依赖关系，系统的组件通过函数调用来进行交互。这种模式叫做单体结构；&lt;/li&gt;
&lt;li&gt;部署层次：控制部署单元（如 Jar 文件）之间的依赖关系，系统的组件可能使用跨线程（注意不是跨网络）的通信、Socket 通信或共享内存融通信；&lt;/li&gt;
&lt;li&gt;服务层次：将组件间的依赖降低到数据结构级别，然后通过网络数据包进行通信，如微服务通过 rpc、rest 交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;并没有严格的标准说明那个解耦层次是更好的，因为随着项目的逐渐成熟，最好的解耦模式可能会发生变化。一个设计良好的架构应该能允许一个系统从单体结构开始，以单一文件的形式部署，然后逐渐成长为一组相互独立的可部署单元，甚至是独立的服务或者微服务。最后还能随着情况的变化，允许系统逐渐回退到单体结构。一个设计良好的架构在上述过程中还应该能保护系统的大部分源码不受变更影响。对整个系统来说，&lt;strong&gt;解耦模式也应该是一个可选项&lt;/strong&gt;。我们在进行大型部署时可以采用一种模式，而在进行小型部署时则可以采用另一种模式。&lt;/p&gt;
&lt;p&gt;在了解解耦之后，让我们看看解耦对系统独立性到底有何影响。&lt;/p&gt;
&lt;h3 id="解耦对系统运行独立性的意义"&gt;解耦对系统运行独立性的意义&lt;/h3&gt;
&lt;p&gt;如果不同面向之间的用例得到了良好的隔离，那么需要高吞吐量的用例就和需要低吞吐量的用例互相自然分开了。如果 UI 和数据库的部分能从业务逻辑分离出来，那么它们就可以运行在不同的服务器上。而且需要较大带宽的应用也可以在多个服务器上运行多个实例。&lt;/p&gt;
&lt;h3 id="解耦对系统开发独立性的意义"&gt;解耦对系统开发独立性的意义&lt;/h3&gt;
&lt;p&gt;只要系统按照其水平分层和用例进行了恰当的解耦，整个系统的架构就可以支持多团队开发，不管团队组织形式是分功能开发、分组件开发、分层开发，还是按照别的什么变量分工都可以&lt;/p&gt;
&lt;h3 id="解耦对系统部署独立性的意义"&gt;解耦对系统部署独立性的意义&lt;/h3&gt;
&lt;p&gt;如果解耦工作做得好，我们甚至可以在系统运行过程中热切换(hot-swap)其各个分层实现和具体用例。在这种情况下，我们增加新用例就只需要在系统中添加一些新的jar文件，或启动一些服务即可，其他部分将完全不受影响&lt;/p&gt;
&lt;h2 id="什么是代码重复"&gt;什么是代码重复&lt;/h2&gt;
&lt;p&gt;代码中的重复可以分为两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;真正的重复：代码中需要被消灭的部分；&lt;/li&gt;
&lt;li&gt;虚假的重复：看起来重复的代码，实际上是不同的演进路径，也就是拥有不同的变更速率与变更边缘。CRP阐述：对于使用频次不同的代码需要拆分到不同的组件中，如果在阅读代码时没有考虑到这一点就很有可能误认为虚假的重复为重复代码，这些“重复”有时候是必须的，还记得计算工资的例子吗？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（第五篇完）&lt;/p&gt;

          </description>
    <category>软件架构</category><category>架构整洁之道</category></item>
    <item>
      <title>领域驱动设计初识</title>
      <link>https://tommycheese.github.io/blogs/ddd%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E5%88%9D%E8%AF%86/</link>
      <pubDate>Fri, 26 Jul 2024 19:49:45 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/ddd%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E5%88%9D%E8%AF%86/</guid>
      <description>&lt;h1 id="领域驱动设计概述"&gt;领域驱动设计概述&lt;/h1&gt;
&lt;p&gt;领域驱动设计（DDD, Domain-Driven Design）是一种模型驱动设计的方法，它通过领域模型捕捉领域知识，使用领域模型构造&lt;strong&gt;更易维护&lt;/strong&gt;的软件.&lt;/p&gt;
&lt;p&gt;DDD的设计过程分为战略设计与战术设计，其中战略设计面向领域、子域以及限界上下文的设计，而战术设计面向实体、值对象、领域事件等设计，关系如下：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tommycheese.github.io/blogimages/DDD1.png" alt="img"&gt;&lt;/p&gt;
&lt;h2 id="战略设计阶段相关概念"&gt;战略设计阶段相关概念&lt;/h2&gt;
&lt;h3 id="领域"&gt;领域&lt;/h3&gt;
&lt;p&gt;领域是系统要解决问题的领域，如商品信息管理就可以是系统要解决问题的领域.&lt;/p&gt;
&lt;h3 id="子域"&gt;子域&lt;/h3&gt;
&lt;p&gt;根据使用语言的不同可以将领域划分为不同的子域：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心域：决定产品核心竞争力的子域，是最为重要、业务最核心、个性的部分；&lt;/li&gt;
&lt;li&gt;通用域：被多个子域使用的通用功能子域，比如用到的通用系统，例如认证、权限等等，这类应用没有企业特点限制，不需要做太多的定制化；&lt;/li&gt;
&lt;li&gt;支撑域：不包含核心功能与通用功能的子域，具有企业特性，但不具有通用性，例如数据代码类的数据字典等系统；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="限界上下文"&gt;限界上下文&lt;/h3&gt;
&lt;p&gt;限界上下文，即限定使用不同模型来解决不同问题所产生的不同区域.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;限界上下文是一个子域或者多个子域的集合&lt;/strong&gt;，&lt;strong&gt;要保证一个限界上下文须支持一个完整的业务流程&lt;/strong&gt;，&lt;strong&gt;保证这个业务流程所涉及的领域都在一个限界上下文中&lt;/strong&gt;.限界上下文是微服务拆分的依据，即每个限界上下文对应一个微服务.&lt;/p&gt;
&lt;p&gt;那么为什么要进行限界上下文呢？这是因为&lt;strong&gt;上下文&lt;/strong&gt;的存在导致编码十分困难，举个例子说明：&lt;/p&gt;
&lt;p&gt;例如在招聘域我们可能会使用“平台”来表明来自于哪个学校、机构；在跳水域我们使用“平台”来表明选手从那个跳台跳水；而在铁路交通运输领域，我们可能会使用“平台”来描述铁路站台……在某一天的某一个机会，跳水选手、HR和乘务员聚到了一起，并且他们都不了解对方的身份，如果此时HR问跳水选手说你来自哪个平台……&lt;/p&gt;
&lt;p&gt;看到了吧，同样是使用“平台”这个词语，但三个人在&lt;strong&gt;没有约定&lt;/strong&gt;的情况下可能对于词语的理解不尽相同，解决这个问题的办法就是把跳水域、招聘域和交通域隔离，分别定义域内的术语，在一个域内讨论就可以最大可能的避免歧义，这就是约定.这也是为什么最好把业务所涉及的领域定义在一个限界上下文中：减少歧义.&lt;/p&gt;
&lt;p&gt;在软件系统的设计中同样会遇到类似的问题，如果你负责设计一个足够大的计算机软件产品，产品涉及到跳水服务、招聘服务与乘车服务，系统没有对服务做任何切分，你在跳水有关业务逻辑中使用platform数据结构描述跳台、在招聘业务中使用platform代表平台、在交通业务中使用platform代表站台，当这三个业务不可避免的交汇到一起时，灾难就发生了，面对满屏幕不同语义但几乎同名的变量，我们能怎么办？&lt;/p&gt;
&lt;p&gt;显然，限定某一个领域术语的细节，我们就可以在领域中畅通无阻的使用这个术语了，这就是限界上下文的重要作用.&lt;/p&gt;
&lt;h2 id="战术设计阶段相关概念"&gt;战术设计阶段相关概念&lt;/h2&gt;
&lt;h3 id="值对象和实体"&gt;值对象和实体&lt;/h3&gt;
&lt;p&gt;首先了解一下值对象和实体.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;值对象是通过属性值来识别的对象，即如果两个值对象的内部值都是相同的，那么我们就认为这两个值对象是相同的.显然，值对象的属性不可变.&lt;/li&gt;
&lt;li&gt;实体是拥有唯一标识和状态，且具有生命周期的业务对象.实体的属性是可变的，如果两个实体的属性是完全相同的，我们也不认为他们是相同的实体，只有他们两个的标识（如ID）是相同的才会被认为是相同的实体.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举例区分值对象和实体：&lt;/p&gt;
&lt;p&gt;假如现在我们有一个白色值对象Color white{R:255, G:255, B:255}和一个轮胎实体tire{Air:&lt;em&gt;,Size:&lt;/em&gt;}：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可变性：白色中的每一个属性都是不可变的，因为一旦变化此对象就不再代表”白色“；而轮胎中的气压、尺寸等值可以改变，因为了即便这些参数发生了变化它仍然是轮胎；&lt;/li&gt;
&lt;li&gt;可比较性：正是由于值对象的值不可变性赋予了其相等的规则，两个白色对象的值一定会是相同的，因此如果两个值对象的内部值都是相同的，那么我们就认为这两个值对象是相同的，实体则由于其属性的可变性而丧失了这个特点.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于实体而言，其形态一般有四种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;失血模型：模型仅仅包含数据的定义和getter/setter方法，业务逻辑和应用逻辑都放到服务层中.这种类在Java中叫POJO.&lt;/li&gt;
&lt;li&gt;贫血模型：贫血模型中包含了一些业务逻辑，但不包含依赖持久层的业务逻辑.这部分依赖于持久层的业务逻辑将会放到服务层中.&lt;/li&gt;
&lt;li&gt;充血模型：充血模型中包含了所有的业务逻辑，包括依赖于持久层的业务逻辑.&lt;/li&gt;
&lt;li&gt;胀血模型：胀血模型就是把和业务逻辑不相关的其他应用逻辑（如授权、事务等）全部都放到领域模型中.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;资源库Repo&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Repo是针对Entity设计的存储操作，Repo中的操作应该尽可能低级、命名要简短，并且不能定义太多.Java中的MyBatis Mapper可以看作是Repo的一种实现.&lt;/p&gt;
&lt;h3 id="聚合与聚合根"&gt;聚合与聚合根&lt;/h3&gt;
&lt;p&gt;聚合是一种更大范围的封装，把一组有相同生命周期、在业务上不可分隔的实体和值对象放在一起考虑，只有聚合根可以对外暴露引用，聚合也是一种内聚性的表现.聚合根之间也可相互调用，聚合根抽象出来一般名字为名词.&lt;/p&gt;
&lt;p&gt;聚合根通过以下几种手段实现封装：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必须通过操作聚合根来实现操作整个聚合，外部操作不允许直接操作聚合中的元素.比如紫色（外观）+轮胎+钢架+…=汽车，我们开车时不能也不会去单独操作轮胎、方向盘、外观…，相反，我们通过操作汽车这个聚合根来间接操作其他实体/值对象.&lt;/li&gt;
&lt;li&gt;聚合定义了一组&lt;strong&gt;边界&lt;/strong&gt;，边界内所有的组件必须对业务逻辑有效；&lt;/li&gt;
&lt;li&gt;必须在一个原子性的事务中操作聚合，否则可能会出现异常，也就是说聚合是操作的单元，从仓库取出聚合、操作完成后放回是一个原子性的操作；如何理解原子性？加入汽车聚合根有轮子实体、钢/铝/碳架实体，你不能拆掉汽车的一个轮子检修完后不装上.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="领域事件domain-events"&gt;领域事件Domain Events&lt;/h3&gt;
&lt;p&gt;当实体的属性发生变化时就会产生领域事件，领域事件是领域专家认为重要的事情.领域事件是发生在领域中且值得注意的事件.而领域事件通常意味着领域对象状态的改变.&lt;strong&gt;领域事件在系统中起到了传递消息、触发其他动作的作用，是解耦领域模型的重要手段之一&lt;/strong&gt;.我们往往利用&lt;strong&gt;消息队列&lt;/strong&gt;来传递领域事件，这样所有订阅此消息的子域都会进行自己内部的响应操作.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;消息总线也是一种实现方法👋.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如轮胎实体气压发生变化时，会释放领域leaked漏气事件，此漏气事件传递了“轮胎漏气”这个消息，从而引发一系列其他的响应，如动能系统变化、方向盘手感变化等等，领域事件通常会使用过去式命名来说明事件已经发生，不可撤销.&lt;/p&gt;
&lt;h3 id="领域服务domain-service"&gt;领域服务Domain Service&lt;/h3&gt;
&lt;p&gt;有些领域中的动作看上去&lt;strong&gt;并不属于任何对象&lt;/strong&gt;.它们代表了领域中的一个重要的行为，不能忽略它们或者简单地把它们合并到某个实体或者值对象中.当这样的行为从领域中被识别出来时，推荐的实践方式是将它声明成一个服务，这个服务就是领域服务.&lt;/p&gt;
&lt;p&gt;领域服务不是微服务中的“服务”概念.服务和聚合根概念有些相近，他们都可以操作多个实体，但是二者理念和作用不同，&lt;strong&gt;聚合根是对实体的组合，而服务是用于同步多个实体的状态，比如将某个item实体加入到list实体&lt;/strong&gt;（比如把mail投递到inbox）.所以Servie一般抽象出来都是动词.&lt;/p&gt;
&lt;h2 id="ddd领域建模设计领域模型"&gt;DDD领域建模（设计领域模型）&lt;/h2&gt;
&lt;p&gt;DDD领域建模一般步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;根据需求划分出初步的子域和限界上下文，以及上下文之间的关系；&lt;/li&gt;
&lt;li&gt;进一步分析每个上下文内部，识别出哪些是实体，哪些是值对象，并确定实体需要使用哪种代码形态：失血、贫血、充血、胀血；&lt;/li&gt;
&lt;li&gt;对实体、值对象进行关联和聚合，划分出聚合的范畴和聚合根；&lt;/li&gt;
&lt;li&gt;为聚合根设计资源库repo，并思考实体或值对象的创建方式；&lt;/li&gt;
&lt;li&gt;在工程中实践领域模型，并在实践中检验模型的合理性，倒推模型中不足的地方并重构.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这就是DDD采用两阶段设计原则——先进行战略设计、随后进行战术设计。&lt;/p&gt;
&lt;p&gt;在实践中，建议实体采用失血模型（实体方法只包括setter/getter）或者贫血模型（包含不涉及数据库操作的简单逻辑，如属性合法性校验）；&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实际上这是DDD建模方式之一，是自顶向下的设计方法，还存在一种方式是使用自下而上的方法，即先确定领域模型——实体、值对象等，再确定子域、限界上下文…&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;（本节完）&lt;/p&gt;

          </description>
    <category>软件架构</category><category>领域驱动设计</category></item>
    <item>
      <title>再读整洁架构之道（四）组件构建原则</title>
      <link>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E5%9B%9B/</link>
      <pubDate>Sat, 06 Jul 2024 17:27:18 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E5%9B%9B/</guid>
      <description>&lt;h1 id="再读整洁架构之道四组件构建原则"&gt;再读整洁架构之道（四）组件构建原则&lt;/h1&gt;
&lt;p&gt;第三篇主要叙述如何设计模块和类，在本篇将会更近一步，叙述如何进行组件的设计。&lt;/p&gt;
&lt;p&gt;组件是软件的部署单元，是整个软件系统在部署过程中可以独立完成部署的最小实体，组件可以被单独开发，组件化的插件式架构已经成为我们习以为常的软件构建形式了。&lt;/p&gt;
&lt;h2 id="组件聚合"&gt;组件聚合&lt;/h2&gt;
&lt;p&gt;组件聚合告诉我们哪些模块和类应该组合在一起形成组件。主要有三个原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;复用/发布等同原则；&lt;/li&gt;
&lt;li&gt;共同闭包原则；&lt;/li&gt;
&lt;li&gt;共同复用原则；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="复用发布等同原则rep"&gt;复用/发布等同原则REP&lt;/h3&gt;
&lt;p&gt;REP指出软件复用的最小粒度应等同于其发布的最小粒度。&lt;/p&gt;
&lt;p&gt;REP是从代码复用角度考虑的，它规定了具备相同主题和功能的代码可以进行组合形成组件。REP建议软件复用应该按照组件来进行的。&lt;/p&gt;
&lt;p&gt;通俗来讲，某个组件打包发布后可能会形成版本号或唯一标识，我们通过引入现成代码库的方式复用代码，引入的单位就是组件打包的结果，引入的凭据就是版本号。&lt;/p&gt;
&lt;h3 id="共同闭包ccp"&gt;共同闭包CCP&lt;/h3&gt;
&lt;p&gt;CCP指出我们应该将那些会同时修改，并且为相同目的而修改的类放到同一个组件中，而将不会同时修改，并且不会为了相同目的而修改的那些类放到不同的组件中。&lt;/p&gt;
&lt;p&gt;CCP是从代码维护角度考虑的，对于由于同一原因/修改目的代码应该组合形成组件，这样就可以有效地降低因软件发布、验证及部署所带来的工作压力。&lt;/p&gt;
&lt;p&gt;前面说过，CCP是SRP的组件版，SRP和CCP都可以用以下的语言概括：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;将由于相同原因而修改，并且需要同时修改的“东西”放在一起。将由于不同原因而修改，并且不同时修改的东西分开。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在SRP中，东西是指函数和其他组成类或模块的成分；&lt;/li&gt;
&lt;li&gt;在CCP中，东西是指构成组件的类和模块。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="共同复用crp"&gt;共同复用CRP&lt;/h3&gt;
&lt;p&gt;CRP指出不要强迫一个组件的用户依赖他们不需要的东西。&lt;/p&gt;
&lt;p&gt;CRP规定了对于使用场景不同、使用频次不同的代码需要进行拆分到不同的组件中。CRP 是为了避免不必要的切分，是ISP接口隔离原则的普适版。&lt;/p&gt;
&lt;p&gt;CRP是接口隔离的普适版，ISP和CRP都可以用以下的语言概括：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不要依赖不需要用到的“东西”。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在ISP中，东西是指包含不需要的方法的函数/类；&lt;/li&gt;
&lt;li&gt;在CCP中，东西是指包含不需要的函数的类/模块；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="思考"&gt;思考&lt;/h3&gt;
&lt;p&gt;那么REP、CCP、CRP三者之间有什么关系呢？&lt;/p&gt;
&lt;p&gt;实际上三者存在着&lt;strong&gt;竞争关系&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;REP和CCP是黏合性原则，它们指导我们应该把哪些类/模块放在一起形成组件，而CRP属于排他性原则，它知道我们哪些类/模块不应该放在一起。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tommycheese.github.io/blogimages/image-20240706143525187.png" alt="image-20240706143525187"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果架构师只关注REP和CCP，那么会发现软件依赖了的组件中包含了很多并不需要的部分，这样会导致太多不必要的发布；&lt;/li&gt;
&lt;li&gt;如果架构师只关注CRP和CCP，那么会发现软件的复用会变得非常困难；&lt;/li&gt;
&lt;li&gt;如果架构师只关注REP和CRP，那么会发现如果部分类/模块需要变更，很多相关模块也不可避免的要跟随变更；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;软件架构师的任务就是要在这三个原则中间进行取舍，组件的构成安排应随着项目重心的不同，以及研发性与复用性的不同而不断演化，因此确定项目朝着哪个方向演进是要动态判断的。&lt;/p&gt;
&lt;h2 id="组件耦合"&gt;组件耦合&lt;/h2&gt;
&lt;p&gt;组件耦合告诉我们如何安排组件之间的关系。它包含三个原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无依赖环原则ADP；&lt;/li&gt;
&lt;li&gt;稳定依赖原则SDP；&lt;/li&gt;
&lt;li&gt;稳定抽象原则SAP；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="无依赖环原则adp"&gt;无依赖环原则ADP&lt;/h3&gt;
&lt;p&gt;ADP告诉我们组件依赖关系图中不应该出现环。&lt;/p&gt;
&lt;p&gt;版本号控制机制解决了一觉醒来综合征的问题，而此机制需要遵循ADP，首先让我们读一下作者是怎么介绍一觉醒来综合征的：&lt;/p&gt;
&lt;p&gt;当你花了一整天的时间，好不容易搞定了一段代码，第二天上班时却发现这段代码莫名其妙地又不能工作了。这大概率是其他工作人员修改了你项目所依赖的组件。&lt;/p&gt;
&lt;p&gt;要想解决这个问题一般有两种方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每周构建&lt;/li&gt;
&lt;li&gt;版本号控制机制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;u&gt;每周构建&lt;/u&gt;&lt;/strong&gt;：每个人先在自己的代码仓工作，每周固定时间（比如周五）再进行项目构建并处理可能的冲突问题。&lt;/p&gt;
&lt;p&gt;这种方式的局限性很明显：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;随着项目越来越大，集成工作会越来越难以按时完成&lt;/li&gt;
&lt;li&gt;整个项目会变得越来越难以构建与测试，团队反馈周期会越来越长，研发质量自然也会越来越差&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此版本号控制机制需要被引入。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;u&gt;版本号控制机制&lt;/u&gt;&lt;/strong&gt;：每当一个组件发布新版本时，其他依赖这个组件的团队都可以自主决定是否立即采用新版本。&lt;/p&gt;
&lt;p&gt;版本号控制机制&lt;strong&gt;不允许组件结构依赖关系图中出现环&lt;/strong&gt;，也就是要遵守无依赖换原则ADP，否则一觉醒来综合征是不可避免的。为什么呢？&lt;/p&gt;
&lt;p&gt;这是因为如果存在依赖环路，环路里的所有组件被组合成了一个更大的组件，这就要求他们都必须使用相同的版本，这样其他的组件才能成功的完成依赖。同样，存在依赖换的系统在测试时也会成为一个棘手的问题，虽然打桩mock技术已经被广泛应用，但同时为环路中的组件重复打桩也已经十分的不优雅了。&lt;/p&gt;
&lt;p&gt;那么如何消除循环依赖呢？方案有两个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用依赖反转创建接口；&lt;/li&gt;
&lt;li&gt;创建新组件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应用DIP解决循环依赖很好理解，下面讲述一个在工作中遇到的案例来说明如何使用创建新组件来打破循环依赖。&lt;/p&gt;
&lt;p&gt;现在有这样的一个场景，团队正在使用领域驱动设计的方法设计系统架构，现在实体Entity Dog需要依赖Dog Repo完成Save存储操作。此时Entity模块依赖了Repo模块，很不幸的是在Entity模块中我们还定义了Dog PO（简直糟透了），Repo的Save又需要使用PO来完成对象的转换并存储。此时Entity和Repo之间就发生了循环依赖，幸运的是，在Go中模块之间的相互依赖是不允许的，检查起会提醒“循环导入”错误，怎么处理这个问题呢？&lt;/p&gt;
&lt;p&gt;答案是我们需要新创建一个PO模块，并把Entity中的PO移动到模块，Repo中依赖PO的函数也要划分到PO中，这样就消除了依赖环。使用依赖注入方法也是一个选择，如果你使用SprintBoot，@AutoWired即可解决此问题，但其本质也是创建了一个新组件——依赖池，消除了依赖环。&lt;/p&gt;
&lt;p&gt;类似的例子还有很多很多，是否还记得我们使用anaconda下载Python库时遇到的库版本冲突问题……&lt;/p&gt;
&lt;p&gt;在这里多插一句，笔者在设计组件结构的时候很自然的就想到要和系统功能对应，也就是说，组件应该是和系统功能一一对应的，这样组件依赖图就和系统功能模块划分也一一对应起来。想当然认为在系统设计的一开始组件依赖结构图也就产出了。&lt;/p&gt;
&lt;p&gt;但作者特别提到，自顶向下的设计是不可能的。让我们看看书中是怎么描述的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;/p&gt;
&lt;p&gt;组件结构图必须随着软件系统的变化而变化和扩张，而不可能在系统构建的最初就被完美设计出。因为组件依赖结构图并不和功能一一对应，它更像是应用程序在构建性与维护性方面的一张地图。&lt;/p&gt;
&lt;p&gt;被设计并实现出来的模块越来越多，项目中就逐渐出现了要对组件依赖关系进行管理的需求，我们希望将项目变更所影响的范围被限制得越小越好，因此需要应用单一职责原则(SRP)和共同闭包原则(CCP)来将经常同时被变更的类聚合在一起。&lt;/p&gt;
&lt;p&gt;组件结构图中的一个重要目标是指导如何隔离频繁的变更。我们不希望那些频繁变更的组件影响到其他本来应该很稳定的组件。&lt;/p&gt;
&lt;p&gt;另外，随着应用程序的增长，创建可重用组件的需要也会逐渐重要起来。这时CRP又会开始影响组件的组成。最后当循环依赖出现时，随着无循环依赖原则(ADP)的应用，组件依赖关系会产生相应的抖动和扩张。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="稳定依赖原则sdp"&gt;稳定依赖原则SDP&lt;/h3&gt;
&lt;p&gt;SDP告诉我们依赖关系必须要指向更稳定的方向。通常来说，要做到软件架构的越底层越稳定。&lt;/p&gt;
&lt;p&gt;任何一个我们预期会经常变更的组件都不应该被一个难于修改的组件所依赖，否则这个多变的组件也将会变得非常难以被修改。这就是软件开发的困难之处，我们精心设计的一个容易被修改的组件很可能会由于别人的一条简单依赖而变得非常难以被修改，通过遵循SDP，这样的问题就能够被避免。&lt;/p&gt;
&lt;p&gt;判断组件是否稳定，可以使用稳定性指标。作者定义稳定性指标可以通过如下公式计算：
$$
I=\frac{Fan-out}{Fan-in+Fan-out}
$$
其中Fan-in为组件的入依赖数量，Fan-out为出依赖数量，这意味着一个组件的出依赖越少越稳定，因为如果出依赖为 0 意味着没有因素可以让组件改变。&lt;/p&gt;
&lt;p&gt;稳定依赖原则(SDP)的要求就是要让每个组件的I指标都必须大于其所依赖组件的I指标（即被指向的组件更稳定）。&lt;/p&gt;
&lt;p&gt;SDP并不是要求所有组件都应该是稳定的，我们设计组件架构图的目的就是要决定应该让哪些组件稳定，让哪些组件不稳定，所有组件都稳定的架构是不灵活的，不灵活的架构没有足够的架构价值。&lt;/p&gt;
&lt;h3 id="稳定抽象原则sap"&gt;稳定抽象原则SAP&lt;/h3&gt;
&lt;p&gt;SAP指出一个组件的抽象化程度应该与其稳定性保持一致。&lt;/p&gt;
&lt;p&gt;高层策略等应该存在稳定的组件中，但这样做会导致高层策略很难修改，幸运的是开闭原则 OCP 告诉我们稳定的组件可以被设计成容易扩展的，抽象类具有稳定抽象能力，可以设计一个抽象类具有较好的稳定性并且易于扩展和修改。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;抽象类是接口和类之间的缓冲地带。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;稳定抽象原则(SAP)为组件的稳定性与它的抽象化程度建立了一种关联。原则要求稳定的组件同时应该是抽象的，这样它的稳定性就不会影响到扩展性。同时一个不稳定的组件应该包含具体的实现代码，这样它的不稳定性就可以通过具体的代码被轻易修改。因此，如果一个组件想要成为稳定组件，那么它就应该由接口和抽象类组成，以便将来做扩展。&lt;/p&gt;
&lt;p&gt;和稳定性类似，我们也可以使用抽象程度指标来衡量组件的抽象程度：
$$
A=\frac{N_c}{N_a}
$$
其中Nc是指组件中类的数量，而Na是组件中抽象类和接口的数量。A指标的取值范围是从0到1，值为0代表组件中没有任何抽象类，值为1就意味着组件中只有抽象类。&lt;/p&gt;
&lt;h3 id="思考-1"&gt;思考🤔&lt;/h3&gt;
&lt;p&gt;SDP和SAP有什么关联吗？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一点，从SDP、SAP与DIP上说&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;实际上，SDP+SAP=组件DIP。因为SDP要求的是让依赖关系指向更稳定的方向，而SAP则告诉我们稳定性本身就隐含了对抽象化的要求，即依赖关系应该指向更抽象的方向。&lt;/p&gt;
&lt;p&gt;怎么理解这句话呢？&lt;/p&gt;
&lt;p&gt;可以想到，DIP在类上的作用是为了保证灵活性，SDP+SAP实际上就是为了保证组件在稳定的基础上足够灵活，并且 DIP 和 SDP+SAP 在软件架构上的影响都是具体实现类指向抽象类/接口，因此从作用上看SDP+SAP=组件DIP。&lt;/p&gt;
&lt;p&gt;但是DIP和SDP+SAP还是有不同之处，对类来说，设计是没有灰色地带的，一个类要么是抽象类，要么就不是。SDP与SAP这对原则是应用在组件层面上的，我们要允许一个组件部分抽象，部分稳定。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其次，从SAP和SDP的目的达成来说&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;此外，SDP指出依赖应该指向稳定的位置，SAP要求我们在设计组件时，高层的策略应该设计在抽象的组件，以便我们在架构设计时将策略与细节分开，同时便于我们进行插件式开发，SDP和SAP他们在目的上是一致的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;看到了吧，这里又提到了&lt;strong&gt;插件式开发&lt;/strong&gt;这个词，上次是在哪里提到的？对，依赖反转DIP，这也说明了SAP+SDP确实类似于组件DIP。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="主序列"&gt;主序列&lt;/h3&gt;
&lt;p&gt;把组件稳定性指标I与组件抽象程度指标A结合在一起，画出一张图，a=-i+1这条直线就叫做主序列线。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tommycheese.github.io/blogimages/image-20240706171725797.png" alt="image-20240706171725797"&gt;&lt;/p&gt;
&lt;p&gt;整个区域可以分为三个区域：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;痛苦区：靠近(0,0)的区域，在这个区域内的组件十分稳定但同时又十分的具体，因此不易改变，如数据库表；工具库也是处于痛苦区的组件，虽然其 I 指标为 1（工具库依赖非常多的组件，因此其不稳定），但不能被修改，否则很多代码会出现问题；&lt;/li&gt;
&lt;li&gt;无用区：处于(1,1)附近的组件，该位置上的组件通常是无限抽象的，但是没有被其他组件依赖，这样的组件往往无法使用，对于这个区域中的软件组件来说，其源码或者类中的设计问题通常是由于历史原因造成的，比如忘记清除的旧代码；&lt;/li&gt;
&lt;li&gt;主序列线区：在整条主序列线上的区域，组件所能处于最优的位置是线的两端(0, 1)与(1, 0)。一个优秀的软件架构师应该争取将自己设计的大部分组件尽可能地推向这两个位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;怎么衡量组件的综合性能呢？答案是计算它与主序列线的距离，来量化一个系统设计与主序列的契合程度，这个值为D值，值为0意味着组件是直接位于主序列线上的，值为1则意味着组件在距离主序列最远的位置。
$$
D=|A+I-1|
$$
这样我们就可以用D指标大于0多少来指导组件的重构与重新设计。&lt;/p&gt;
&lt;p&gt;除此以外，D还可以有更多的作用，比如计算设计中所有组件的D指标的平均值和方差，用统计学的方法来量化分析一个系统设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于一个良好的系统设计来说，D指标的平均值和方差都应该接近于0；&lt;/li&gt;
&lt;li&gt;方差作为组件的“达标红线”来使用，我们可以通过它找出系统设计中那些不合常规的组件；&lt;/li&gt;
&lt;li&gt;还可以按照时间追踪 D 的方差值，来观察随着时间的变化，软件系统架构的稳定和抽象变化。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（第四篇完）&lt;/p&gt;

          </description>
    <category>软件架构</category><category>架构整洁之道</category></item>
    <item>
      <title>再读整洁架构之道（三）SOLID设计原则</title>
      <link>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%B8%89/</link>
      <pubDate>Fri, 05 Jul 2024 14:11:30 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%B8%89/</guid>
      <description>&lt;h1 id="再读整洁架构之道三solid原则"&gt;再读整洁架构之道（三）SOLID原则&lt;/h1&gt;
&lt;p&gt;构建软件模块的主要目标有三个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使软件可容忍被改动&lt;/li&gt;
&lt;li&gt;使软件更容易被理解&lt;/li&gt;
&lt;li&gt;构建可在多个软件系统中复用的组件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SOLID原则的主要作用就是告诉我们如何将数据和函数组织成为类，以及如何将这些类链接起来成为程序。SOLID原则指导我们如何设计模块，在架构设计层面，我们会有其他的设计原则。&lt;/p&gt;
&lt;p&gt;SOLID原则是指单一职责原则&lt;strong&gt;S&lt;/strong&gt;RP、开闭原则&lt;strong&gt;O&lt;/strong&gt;CP、里式替换原则&lt;strong&gt;L&lt;/strong&gt;SP、接口隔离原则&lt;strong&gt;I&lt;/strong&gt;SP和接口反转&lt;strong&gt;D&lt;/strong&gt;IP。&lt;/p&gt;
&lt;h2 id="单一职责原则srp"&gt;单一职责原则SRP&lt;/h2&gt;
&lt;p&gt;SRP定义每个软件模块只对一个功能负责，&lt;strong&gt;只有一个理由可以让模块改变&lt;/strong&gt;，任何一个软件模块都应该只对某一类行为者负责。&lt;/p&gt;
&lt;p&gt;组件层面的SRP被称作共同闭包原则CCP。&lt;/p&gt;
&lt;p&gt;倘若设计模块时没有遵循SRP会发生什么呢？一起看一个例子。&lt;/p&gt;
&lt;p&gt;如果财务部门、人事部门、研发部门共同依赖于一个工资、工时计算的程序，程序包含三个函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CalculateSalary：计算工资；&lt;/li&gt;
&lt;li&gt;CalculateTime：计算工时；&lt;/li&gt;
&lt;li&gt;Save：保存信息；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三个部门平静地使用着程序，突然有一天，财务部的薪资计算发生变化，因此财务部门的维护人员更改了CalculateSalary函数；然后，诡异的事情发生了，人事部门、研发部薪资也同时变化了…&lt;/p&gt;
&lt;h2 id="开闭原则ocp"&gt;开闭原则OCP&lt;/h2&gt;
&lt;p&gt;“拥抱新增、抗拒修改！”&lt;/p&gt;
&lt;p&gt;OCP指出：如果软件系统想要更容易被改变，那么其设计就必须允许新增代码来修改系统行为，而非只能靠修改原来的代码。设计良好的计算机软件应该易于扩展，同时抗拒修改。&lt;/p&gt;
&lt;p&gt;OCP不仅适用于类和模块的设计，也适用于组件的设计。他起源于设备无关性的概念。具体来说，我们使用依赖反转将IO设备设计成插件，这样的我们就能很轻易的新增IO设备，而不去修改原有的IO设备。&lt;/p&gt;
&lt;p&gt;OCP的实现方式是通过将系统划分为一系列组件，并且将这些组件间的依赖关系按层次结构进行组织，使得高阶组件不会因低阶组件被修改而受到影响。&lt;/p&gt;
&lt;h2 id="里式替换lsp原则"&gt;里式替换LSP原则&lt;/h2&gt;
&lt;p&gt;LSP指出：如果想用可替换的组件来构建软件系统，那么这些组件就必须遵守同一个约定，以便让这些组件可以相互替换。&lt;/p&gt;
&lt;p&gt;LSP的本质是一个可替换性原则：如果对于每个类型是S的对象o1都存在一个类型为T的对象o2，能使操作T类型的程序P在用o1替换o2时行为保持不变，我们就可以将S称为T的子类型。&lt;/p&gt;
&lt;p&gt;LSP可以且应该被应用于软件架构层面，因为一旦违背了可替换性，该系统架构就不得不为此增添大量复杂的应对机制。&lt;/p&gt;
&lt;h2 id="接口隔离原则isp"&gt;接口隔离原则ISP&lt;/h2&gt;
&lt;p&gt;ISP指出：应该在设计中避免不必要的依赖，任何层次的软件设计如果依赖了它并不需要的东西，就会带来意料之外的麻烦。&lt;/p&gt;
&lt;h2 id="依赖反转dip"&gt;依赖反转DIP&lt;/h2&gt;
&lt;p&gt;DIP指出该设计原则指出高层策略性的代码不应该依赖实现底层细节的代码，恰恰相反，那些实现底层细节的代码应该依赖高层策略性的代码。&lt;/p&gt;
&lt;p&gt;通俗理解依赖反转：这需要使用控制流和源代码依赖流来说明。控制流譬如A组件流向B组件，此时B需要知道A的实现，A也需要引入B的模块，代码依赖上也就不可避免的A组件流向了B组件。此时系统行为决定了控制流，而控制流则决定了源代码依赖关系，软件架构就别无选择。但是通过引入接口，B组件此时只需要实现接口，接口引入B，A引入接口，此时A就无需引入B模块，此时的控制流和源代码依赖是反向的，也就是“依赖反转”。&lt;/p&gt;
&lt;p&gt;依赖反转给了软件架构设计的自由，如果想要设计一个灵活的系统，在源代码层次的依赖关系中就应该多引用抽象类型（接口、抽象类等），而非具体实现。&lt;/p&gt;
&lt;p&gt;DIP可以归纳出几条编码守则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;应在代码中多使用抽象接口，尽量避免使用那些多变的具体实现类；&lt;/li&gt;
&lt;li&gt;不要在具体实现类上创建衍生类；&lt;/li&gt;
&lt;li&gt;不要覆盖(override)包含具体实现的函数；&lt;/li&gt;
&lt;li&gt;应避免在代码中写入与任何具体实现相关的名字，或者是其他容易变动的事物的名字。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（第三篇完）&lt;/p&gt;

          </description>
    <category>软件架构</category><category>架构整洁之道</category></item>
    <item>
      <title>再读整洁架构之道（二）编程范式</title>
      <link>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%BA%8C/</link>
      <pubDate>Fri, 05 Jul 2024 07:25:33 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%BA%8C/</guid>
      <description>&lt;h1 id="再读整洁架构之道二"&gt;再读整洁架构之道（二）&lt;/h1&gt;
&lt;p&gt;编程范式指的是程序的编写模式，它告诉我们应该在什么时候采用什么样的代码结构。&lt;/p&gt;
&lt;p&gt;截止目前，一共出现了三种编程范式：结构化编程范式、面向对象编程范式和函数式编程。&lt;/p&gt;
&lt;p&gt;作者认为，每种编程方式不是在给架构设计者的武器库进行扩充，相反，架构师和程序员的武器已经够多了，这三种编程范式是在对他们利用的武器进行&lt;strong&gt;限制&lt;/strong&gt;，这也是为什么他们叫做“范式”。&lt;/p&gt;
&lt;h2 id="结构化编程范式"&gt;结构化编程范式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;结构化编程对程序控制权的直接转移进行了限制和规范，特别指的是限制了程序中goto的随意使用。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="分解程序"&gt;分解程序&lt;/h3&gt;
&lt;p&gt;Dijkstra希望使用数学推导方法对程序进行推理证明：让程序就成为了一种欧几里得结构，这样可以用一些已经证明的结构串联起新的程序，从而进一步推导整个程序的正确性。&lt;/p&gt;
&lt;p&gt;他还发现，如果程序中包含大量的goto，那么程序会变得难以分解，而goto语句的作用完全可以依靠分支和循环来完成。&lt;/p&gt;
&lt;p&gt;这样，结构化编程范式表明，程序可以进行降解拆分，一个大型问题拆分为一系列高级函数的组合，而这些高级函数各自又可以继续被拆分为一系列低级函数，如此无限递归。更重要的是，每个被拆分出来的函数也都可以用结构化编程范式来书写。&lt;/p&gt;
&lt;p&gt;但是，这种形式化证明的编程方式没有成为主流，科学证明法是目前使用较多的方法。&lt;/p&gt;
&lt;h3 id="科学证明法"&gt;科学证明法&lt;/h3&gt;
&lt;p&gt;科学理论和科学定律可以被证伪，但是没有办法被证明，类似的，程序只能测试证伪，不能证明，即“测试只能展示Bug的存在，并不能证明不存在Bug”。因此，利用科学证明法，结构化编程范式就可以促使我们先将一段程序递归降解为一系列可证明的小函数，然后再编写相关的&lt;strong&gt;测试&lt;/strong&gt;来试图证明这些函数是错误的。如果这些测试无法证伪这些函数，那么我们就可以认为这些函数是足够正确的，进而推导整个程序是正确的。&lt;/p&gt;
&lt;h2 id="面向对象编程范式"&gt;面向对象编程范式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;面向对象编程对程序控制权的间接转移进行了限制和规范。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;具体来说，面向对象编程凡事使用&lt;strong&gt;多态特性&lt;/strong&gt;限制了函数指针的使用。这可以理解为函数指针只能进行有限制的指向，比如java中的多态一般出现在有继承关系的两个类的对象之间。&lt;/p&gt;
&lt;p&gt;作者也认为多态是OOP的最大特性，利用多态可以做到&lt;strong&gt;依赖反转&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;依赖反转即引入接口，完全控制系统中所有源代码的依赖关系，而不需要收到系统控制流的制约。&lt;/p&gt;
&lt;p&gt;依赖反转保证了我们可以对系统进行插件式的开发，如将web UI和数据库与业务逻辑之间的依赖进行反转，可以保证主业务逻辑与UI和数据库的解耦，这样UI和数据库就成了主业务逻辑的插件。&lt;/p&gt;
&lt;p&gt;面向对象编程就是以多态为手段来对源代码中的依赖关系进行控制的能力，这种能力让软件架构师可以构建出某种插件式架构，让高层策略性组件与底层实现性组件相分离，底层组件可以被编译成插件，实现独立于高层组件的开发和部署。&lt;/p&gt;
&lt;h2 id="函数式编程范式"&gt;函数式编程范式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;函数式编程对程序中的赋值进行了限制和规范。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;换句话说，函数式编程语言中变量是不可变的。&lt;/p&gt;
&lt;p&gt;作者有以下的观点，这一段话来自原文：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“所有的竞争问题、死锁问题、并发更新问题都是由可变变量导致的。如果变量永远不会被更改，那就不可能产生竞争或者并发更新问题。如果锁状态是不可变的，那就永远不会产生死锁问题"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果不考虑储存器与处理器的速度限制，那么不可变性是可行的，但实际上我们必须考虑这些因素，因此可变性只能在一定程度上可行，需要让不可变性在一定程度上可行需要完成&lt;strong&gt;可变性的隔离&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="可变性的隔离"&gt;可变性的隔离&lt;/h3&gt;
&lt;p&gt;可变性隔离的一种常见方式是将应用程序，或者是应用程序的内部服务进行切分，&lt;strong&gt;划分为可变的和不可变的两种组件&lt;/strong&gt;。不可变组件用纯函数的方式来执行任务，期间不更改任何状态。这些不可变的组件将通过与一个或多个非函数式组件（即可变组件）通信的方式来修改变量状态。&lt;/p&gt;
&lt;p&gt;一个例子是GIT版本管理工具。它通过移动指针来记录文件的变化：增删改，但实际上文件并没有被真正的修改和删除，只存在增加和检索查询两种情况，这不就是不可变性的一个例子吗。&lt;/p&gt;
&lt;p&gt;同时，mysql中的事务管理、事务性内存的工作也都运用了这个思想。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;三种编程范式与软件架构具有密切的关系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多态是我们跨越边界的手段；&lt;/li&gt;
&lt;li&gt;函数式编程是我们规范和限制数据存放位置与访问权限的手段；&lt;/li&gt;
&lt;li&gt;结构化编程则是个模块的实现基础；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三种编程范式与软件架构的三大关注重点不谋而合：&lt;strong&gt;组件独立性&lt;/strong&gt;（面向对象）、&lt;strong&gt;数据管理&lt;/strong&gt;（函数式）以及功能性（结构）。&lt;/p&gt;
&lt;h2 id="再次思考"&gt;再次思考&lt;/h2&gt;
&lt;p&gt;三种编程范式都对程序员提出了新的&lt;strong&gt;限制&lt;/strong&gt;。每个范式都约束了某种编写代码的方式，没有一个编程范式是在增加新能力。也就是说，编程范式带来的是——什么不应该做。&lt;/p&gt;
&lt;p&gt;（第二篇完）&lt;/p&gt;

          </description>
    <category>软件架构</category><category>架构整洁之道</category></item>
    <item>
      <title>再读整洁架构之道（一）</title>
      <link>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%B8%80/</link>
      <pubDate>Wed, 03 Jul 2024 23:10:20 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%B8%80/</guid>
      <description>&lt;h2 id="前记"&gt;前记&lt;/h2&gt;
&lt;p&gt;为什么是再读？因为在这之前笔者已经阅读过一遍架构之道这本书，但当时缺乏项目的锻炼和试错，总觉得读的不深，终于有幸在工作期间接触到了一些项目和需求，更幸运的是项目组在开发时采用的架构正好是整洁架构，详细的说，是事件驱动开发DDD+整洁架构，关于DDD的详细内容将会在之后讨论，笔者就重新阅读了这本书📚，收获颇丰，因此这一个系列，我准备用自己的拙见描述一下整洁架构，更准确的说，应该是是读书笔记，以兹同仁。&lt;/p&gt;
&lt;p&gt;这本书的详细地址为：&lt;a href="https://weread.qq.com/web/bookDetail/480322f072021a3248038c8"&gt;架构整洁之道-罗伯特 C. 马丁-微信读书 (qq.com)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;本系列会以图书的章节组织作为思路，跟随作者的脚步逐渐深入，同时，由于是再读，所以不可避免的也会穿插一些图书中其他部分的内容，所以，如果你在阅读时发现了一些难于理解或者没听说过的内容，那它大概率是在之后章节中的内容。&lt;/p&gt;
&lt;p&gt;让我们开始吧。&lt;/p&gt;
&lt;h2 id="设计与架构的含义"&gt;设计与架构的含义&lt;/h2&gt;
&lt;p&gt;设计与架构，本质上并没有区别，底层设计细节和顶层架构信息共同定义了软件系统。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;软件架构的终极目标是用最小的人力成本来满足构建和维护系统的需求&lt;/strong&gt;，因此成本可以评估一个软件架构设计的优劣。&lt;/p&gt;
&lt;p&gt;没有经过设计、匆匆构建的系统可能成为&lt;strong&gt;乱麻系统&lt;/strong&gt;，乱麻系统在构建过程中被长期忽视代码质量和设计结构优化。认真设计软件架构可以在一定程度上避免系统成为乱麻系统，从而降低成本。&lt;/p&gt;
&lt;p&gt;软件开发具有两个核心特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要想跑得快，要先跑得稳；&lt;/li&gt;
&lt;li&gt;过度自信只会使得重构设计陷入和原项目一样的困局。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="两个价值维度"&gt;两个价值维度&lt;/h2&gt;
&lt;p&gt;软件系统包含两种价值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;行为价值：让机器按照某种指定方式运转，给系统的使用者创造或者提高利润；&lt;/li&gt;
&lt;li&gt;架构价值：软件要够灵活，更改软件的成本要做到与需求的范畴相关，与形状无关。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;怎么理解需求的范畴与形状？
笔者自己理解范畴即需求大致的范围和所属&lt;strong&gt;领域&lt;/strong&gt;，而形状指需求的细节内容。&lt;/p&gt;
&lt;p&gt;那么哪个价值维度更重要呢？
作者认为架构价值比行为价值更重要，因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果某程序可以正常工作，但是无法修改，那么当需求变更的时候它就不再能够正常工作了，我们也无法通过修改让它能继续正常工作。因此，这个程序的价值将成为0；&lt;/li&gt;
&lt;li&gt;如果某程序目前无法正常工作，但是我们可以很容易地修改它，那么将它改好，并且随着需求变化不停地修改它，都应该是很容易的事。因此，这个程序会持续产生价值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;业务部门与研发部门经常犯的共同错误是没有把真正紧急并且重要的功能和紧急但是不重要的功能分开，结果就是重要的系统架构问题让位给了不重要的系统行为功能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;平衡系统架构的重要性与功能的紧急程度这件事，是软件研发人员自己的职责&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;笔者注：同时，即便同样是研发人员，不同的小组也会存在类似问题，正如某些前端部门认为后台只需要提供一系列接口很容易，而不知道良好设计的重要性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="为好的软件架构持续斗争"&gt;为好的软件架构持续斗争✊&lt;/h3&gt;
&lt;p&gt;如果忽视软件架构的价值，系统将会变得越来越难以维护，终会有一天，系统将会变得再也无法修改。如果系统变成了这个样子，那么说明软件开发团队没有和需求方做足够的抗争，没有完成自己应尽的职责。&lt;/p&gt;

          </description>
    <category>软件架构</category><category>架构整洁之道</category></item>
    <item>
      <title>实用干货：如何把 PyTorch 模型参数加载到 MindSpore 模型？</title>
      <link>https://tommycheese.github.io/blogs/%E5%AE%9E%E7%94%A8%E5%B9%B2%E8%B4%A7%E5%A6%82%E4%BD%95%E6%8A%8Apytorch%E6%A8%A1%E5%9E%8B%E5%8F%82%E6%95%B0%E5%8A%A0%E8%BD%BD%E5%88%B0mindspore%E6%A8%A1%E5%9E%8B/</link>
      <pubDate>Fri, 01 Sep 2023 22:53:58 +0530</pubDate>
      <guid>https://tommycheese.github.io/blogs/%E5%AE%9E%E7%94%A8%E5%B9%B2%E8%B4%A7%E5%A6%82%E4%BD%95%E6%8A%8Apytorch%E6%A8%A1%E5%9E%8B%E5%8F%82%E6%95%B0%E5%8A%A0%E8%BD%BD%E5%88%B0mindspore%E6%A8%A1%E5%9E%8B/</guid>
      <description>&lt;h3 id="问题简述"&gt;问题简述&lt;/h3&gt;
&lt;p&gt;在日常的模型开发、训练过程中我们经常会遇到这样的现象：在现有的开源项目或者论文复现中，多数模型使用Pytorch设计、开发和训练推理，当我们需要使用MindSpore框架进行模型开发时，会遇到以下两个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模型使用Pytorch编码；&lt;/li&gt;
&lt;li&gt;Pytorch模型训练后保存的参数无法被MindSpore模型直接加载。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于第一个问题，我们可以根据昇思官方提供的文档：&lt;a href="https://www.mindspore.cn/docs/zh-CN/r2.1/migration_guide/typical_api_comparision.html#%E4%B8%8Epytorch%E5%85%B8%E5%9E%8B%E6%8E%A5%E5%8F%A3%E5%8C%BA%E5%88%AB"&gt;与Pytorch典型区别&lt;/a&gt;和&lt;a href="https://www.mindspore.cn/docs/zh-CN/r2.1/note/api_mapping/pytorch_api_mapping.html#pytorch%E4%B8%8Emindspore-api%E6%98%A0%E5%B0%84%E8%A1%A8"&gt;PyTorch与MindSpore API映射表&lt;/a&gt;来完成模型的迁移；&lt;/p&gt;
&lt;p&gt;对于模型参数的转换，在最新的MindSpore版本中MindConverter不再支持，因此可以考虑针对模型参数，我们进行&lt;strong&gt;手动的转换&lt;/strong&gt;，将Pytorch模型参数转换为MindSpore能识别的格式后，再进行加载。&lt;/p&gt;
&lt;h3 id="解决方案"&gt;解决方案&lt;/h3&gt;
&lt;p&gt;模型的编码转换不再赘述。&lt;/p&gt;
&lt;p&gt;参数转换主要思路如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用Pytorch加载Pytorch模型，并取得模型参数prams_torch；&lt;/li&gt;
&lt;li&gt;使用MindSpore加载MindSpore模型，并取得模型参数prams_ms；&lt;/li&gt;
&lt;li&gt;将Pytorch模型的参数名和MindSpore模型参数名一一对应（有的话）；&lt;/li&gt;
&lt;li&gt;建立torch_2_ms键名映射表，使用键名映射表将Pytorch模型参数值加载到MindSpore参数名对应的位置上；&lt;/li&gt;
&lt;li&gt;使用MindSpore加载参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="案例分析"&gt;案例分析&lt;/h3&gt;
&lt;p&gt;不同模型的模块不相同，参数类型也不尽相同，此处我们以一个网络举例，说明转换的基本思路，不同的模型其转换思路是类似的。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://arxiv.org/abs/1905.11946"&gt;EfficientNet&lt;/a&gt;是谷歌于2019年发表的文章，详细网络架构可查看文章描述，此处我们以&lt;strong&gt;EfficientNet+FC&lt;/strong&gt;全连接层的模型为例，探讨如何进行网络模型参数的转换。&lt;/p&gt;
&lt;h4 id="使用pytorch加载pytorch模型并取得模型参数prams_torch"&gt;使用Pytorch加载Pytorch模型，并取得模型参数prams_torch&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; torch
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; test.efficientnet_pytorch.model &lt;span style="color:#f92672"&gt;import&lt;/span&gt; EfficientNet &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; EN_pytorch
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; pandas &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; pd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pytorch_model &lt;span style="color:#f92672"&gt;=&lt;/span&gt; EN_pytorch&lt;span style="color:#f92672"&gt;.&lt;/span&gt;from_name(cfg[&lt;span style="color:#e6db74"&gt;'model'&lt;/span&gt;], override_params&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;'num_classes'&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pytorch_model&lt;span style="color:#f92672"&gt;.&lt;/span&gt;cuda()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pytorch_weights_dict &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pytorch_model&lt;span style="color:#f92672"&gt;.&lt;/span&gt;state_dict()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;param_torch &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pytorch_weights_dict&lt;span style="color:#f92672"&gt;.&lt;/span&gt;keys()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;param_torch_lst &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame(param_torch)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;param_torch_lst&lt;span style="color:#f92672"&gt;.&lt;/span&gt;to_csv(&lt;span style="color:#e6db74"&gt;'param_torch.csv'&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;步骤结束后，我们就将pytorch的模型参数存到了param_torch.csv下，观察数据：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;keys&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;_conv_stem.weight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;_bn0.weight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;_bn0.bias&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;_bn0.running_mean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;_bn0.running_var&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;_bn0.num_batches_tracked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;_blocks.0._depthwise_conv.weight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;_blocks.0._bn1.weight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;_blocks.0._bn1.bias&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;_blocks.0._bn1.running_mean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;_blocks.0._bn1.running_var&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id="使用mindspore加载mindspore模型并取得模型参数prams_ms"&gt;使用MindSpore加载MindSpore模型，并取得模型参数prams_ms&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; mindspore &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; ms
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; test.efficientnet_mindspore.model &lt;span style="color:#f92672"&gt;import&lt;/span&gt; EfficientNet &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; EN_ms
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; pandas &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; pd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mindspore_model &lt;span style="color:#f92672"&gt;=&lt;/span&gt; EN_ms&lt;span style="color:#f92672"&gt;.&lt;/span&gt;from_name(cfg[&lt;span style="color:#e6db74"&gt;'model'&lt;/span&gt;], override_params&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;'num_classes'&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;prams_ms &lt;span style="color:#f92672"&gt;=&lt;/span&gt; mindspore_model&lt;span style="color:#f92672"&gt;.&lt;/span&gt;parameters_dict()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;keys()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;prams_ms_lst &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame(prams_ms)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;prams_ms_lst&lt;span style="color:#f92672"&gt;.&lt;/span&gt;to_csv(&lt;span style="color:#e6db74"&gt;'prams_ms.csv'&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;步骤结束后，我们就将MindSpore的模型参数存到了prams_ms.csv下，观察数据：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;keys&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;_conv_stem.weight&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;_bn0.moving_mean&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;_bn0.moving_variance&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;_bn0.gamma&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;_bn0.beta&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;0._depthwise_conv.weight&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0._bn1.moving_mean&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;0._bn1.moving_variance&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0._bn1.gamma&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;0._bn1.beta&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0._se_reduce.weight&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id="将pytorch模型的参数名和mindspore模型参数名一一对应"&gt;将Pytorch模型的参数名和MindSpore模型参数名一一对应&lt;/h4&gt;
&lt;p&gt;自此我们就得到了MindSpore和Pytorch各自的参数键名表（附在附件区域），随后观察二者参数命名上的差异，可以发现固定的规律，以下述几个方面为例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Batch Normalization：
&lt;ul&gt;
&lt;li&gt;权重：weight|bias——gamma|beta；&lt;/li&gt;
&lt;li&gt;移动加权和方差：running_mean|running_var——moving_mean|moving_variance；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;自定义blocks：pytorch带前置的_blocks.；&lt;/li&gt;
&lt;li&gt;其他&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="键名映射表"&gt;键名映射表&lt;/h4&gt;
&lt;p&gt;这样就可以根据规律写出一个Python脚本来完成键名的转化，并生成键名映射表：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pytorch&lt;/th&gt;
&lt;th&gt;mindspore&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;_conv_stem.weight&lt;/td&gt;
&lt;td&gt;_conv_stem.weight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_bn0.weight&lt;/td&gt;
&lt;td&gt;_bn0.gamma&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_bn0.bias&lt;/td&gt;
&lt;td&gt;_bn0.beta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_bn0.running_mean&lt;/td&gt;
&lt;td&gt;_bn0.moving_mean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_bn0.running_var&lt;/td&gt;
&lt;td&gt;_bn0.moving_variance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_blocks.0._depthwise_conv.weight&lt;/td&gt;
&lt;td&gt;0._depthwise_conv.weight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_blocks.0._bn1.weight&lt;/td&gt;
&lt;td&gt;0._bn1.gamma&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_blocks.0._bn1.bias&lt;/td&gt;
&lt;td&gt;0._bn1.beta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_blocks.0._bn1.running_mean&lt;/td&gt;
&lt;td&gt;0._bn1.moving_mean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_blocks.0._bn1.running_var&lt;/td&gt;
&lt;td&gt;0._bn1.moving_variance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_blocks.0._se_reduce.weight&lt;/td&gt;
&lt;td&gt;0._se_reduce.weight&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;随后在Pytorch的权重字典中，按照对应文件的Pytorch_key取出权重值，随后使用mindspore.Parameter进行封装，添加到mindspore.key对应的权值中去：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; i &lt;span style="color:#f92672"&gt;in&lt;/span&gt; ms_param_lst&lt;span style="color:#f92672"&gt;.&lt;/span&gt;values:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;    ms_key &lt;span style="color:#f92672"&gt;=&lt;/span&gt; i
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;    pt_key &lt;span style="color:#f92672"&gt;=&lt;/span&gt; param_mapping[ms_key]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;    pt_val &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pt_values_dict[pt_key]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;    &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; isinstance(pt_val, np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;ndarray):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;        pt_val &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pt_val&lt;span style="color:#f92672"&gt;.&lt;/span&gt;cpu()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numpy()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;    ms_val &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Parameter(pt_val, ms_key)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;    print(ms_val)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;    ms_values_dict[ms_key] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ms_val
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="使用mindspore加载参数"&gt;使用MindSpore加载参数&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;load_param_into_net(mindspore_model, ms_values_dict)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;此时，参数应该就可以被MindSpore接受了。&lt;/p&gt;
&lt;h3 id="whats-more"&gt;What’s more&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在参数值的存储过程中，要注意Pytorch和MindSpore参数精度的差异；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（完）&lt;/p&gt;

          </description>
    <category>机器学习</category><category>模型迁移</category></item>
    <item>
      <title>梯度之上：Hessian 矩阵</title>
      <link>https://tommycheese.github.io/blogs/h/</link>
      <pubDate>Fri, 01 Sep 2023 22:53:58 +0530</pubDate>
      <guid>https://tommycheese.github.io/blogs/h/</guid>
      <description>&lt;p&gt;本文讨论研究梯度下降法的一个有力的数学工具：海森矩阵。在讨论海森矩阵之前，需要首先了解梯度和雅克比矩阵的基本概念。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⭐本文假设读者已经熟悉梯度下降法和简单的数值分析、线性代数知识
&lt;a href="https://tommycheese.github.io/blogs/%E6%A2%AF%E5%BA%A6%E4%B9%8B%E4%B8%8Ahessian-%E7%9F%A9%E9%98%B5/"&gt;原文链接&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="梯度雅克比矩阵"&gt;梯度、雅克比矩阵&lt;/h2&gt;
&lt;p&gt;梯度下降算法需要当前函数点的导数信息，当此函数点包含多个方向时，梯度是包含所有方向的（偏）导数向量。&lt;/p&gt;
&lt;p&gt;上述情况对应于&lt;strong&gt;输出为一个&lt;/strong&gt;的情况，当函数的输出也为一个向量时，我们需要把输出向量的每一个元素对于多个输入的梯度&lt;strong&gt;罗列在一起&lt;/strong&gt;，罗列形成的矩阵就是&lt;strong&gt;雅克比矩阵（Jacobian Matrix）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;举例说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若函数$f$接受三个输入$x1、x2、x3$，产生一个输出$y$，则其梯度为：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\begin{equation}
Grad = [\frac{\partial y}{\partial x_1}, \frac{\partial y}{\partial x_2}, \frac{\partial y}{\partial x_3}]
\end{equation}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若函数$f2$接受三个输入$x1、x2、x3$，产生三个输出$y1、y2、y3$，则其雅克比矩阵为：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\begin{equation}
Jacobian  = \begin{bmatrix}
\frac{\partial y_1}{\partial x_1} &amp;amp;  \frac{\partial y_1}{\partial x_2}&amp;amp;\frac{\partial y_1}{\partial x_3} \
\frac{\partial y_2}{\partial x_1} &amp;amp;  \frac{\partial y_2}{\partial x_2}&amp;amp;\frac{\partial y_2}{\partial x_3} \
\frac{\partial y_3}{\partial x_1} &amp;amp;  \frac{\partial y_3}{\partial x_2}&amp;amp;\frac{\partial y_3}{\partial x_3}
\end{bmatrix}
\end{equation}
$$&lt;/p&gt;
&lt;p&gt;利用二阶导数，我们可以知道关于函数在特定方向 $d$ 上的凹凸信息，利用凹凸信息可以在一定程度上预判梯度下降法的表现效果。如果在特定方向 $d$ 上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;二阶导数为正，则函数在方向$d$上一阶导数增加，函数值下降更慢；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;二阶导数为负，则函数在方向$d$上一阶导数减少，函数值下降更快；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;二阶导数为零，则函数在方向$d$上一阶导数不变，函数值匀速下降；&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⭐注意在梯度下降法中是对损失函数进行下降，因此需要使用减函数来分析函数中&lt;strong&gt;某一小段&lt;/strong&gt;（经常使用二次函数的减半部近似：二阶泰勒展开、牛顿法）中的导数变化情况；&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="海森矩阵"&gt;海森矩阵&lt;/h2&gt;
&lt;p&gt;和雅克比矩阵类似，&lt;strong&gt;海森矩阵（Hessian 矩阵）&lt;strong&gt;可以包含函数中二阶导的信息：
$$
Hessian   = \begin{bmatrix}
\frac{\partial^2y}{\partial x_1\partial x_1} &amp;amp;  \frac{\partial^2y}{\partial x_1\partial x_2}&amp;amp;\frac{\partial^2y}{\partial x_1\partial x_3} \
\frac{\partial^2y}{\partial x_2\partial x_1} &amp;amp;  \frac{\partial^2y}{\partial x_2\partial x_2}&amp;amp;\frac{\partial^2y}{\partial x_2\partial x_3} \
\frac{\partial^2y}{\partial x_3\partial x_1} &amp;amp;  \frac{\partial^2y}{\partial x_3\partial x_2}&amp;amp;\frac{\partial^2y}{\partial x_3\partial x_3}
\end{bmatrix}
$$
同时由于二阶导数计算顺序的可交换性，即 $\frac{\partial^2y}{\partial x_1\partial x_2}=\frac{\partial^2y}{\partial x_2\partial x_1}$，因此&lt;/strong&gt;海森矩阵是一个对称矩阵&lt;/strong&gt;，对于对称矩阵我们可以使用&lt;strong&gt;特征分解&lt;/strong&gt;来研究特征值和二阶导数的关系，便于我们快速获得某个方向的二阶导数。&lt;/p&gt;
&lt;p&gt;针对于特定方向d，已知此方向的二阶导数可以写成 $d^THd$ ，则：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🔗 &lt;a href="https://blog.csdn.net/weixin_42397505/article/details/112066943"&gt;基于Hessian矩阵的二阶方向导数与性质_Hi 喀什噶尔的胡杨的博客-CSDN博客_二阶方向导数&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;若d是H对应特征值λ的特征向量：&lt;/p&gt;
&lt;p&gt;因为d为对应λ的特征向量（以下简称特征向量），则据定义有：
$$
Hd = \lambda d\
\Rightarrow  d^THd=d^T\lambda d = \lambda d^Td=\lambda    \ \ \ 对称矩阵d^T = d^-
$$&lt;/p&gt;
&lt;p&gt;因此特征向量对应的特征值λ即为此方向的二阶导数；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若d为其他方向：设$e_i$为$H$对应特征值$\lambda_i$的特征向量，由上可知，
$$
\lambda_i=e_i^THe_i
$$
可知任何一个方向$d=\sum_i^mt_ie_i$为特征向量的线性组合，其中m为特征值个数，$t_i$为第$i$个特征向量的加权数，则：
$$
d^THd=(\sum_i^mt_ie_i)^TH(\sum_i^mt_ie_i)=\sum_i^mt_ie_i^THt_ie_i=\sum_i^mt_i^2\lambda_i
$$
因此任意非特征向量方向的二阶导数是所有特征值的加权和，特别的，此时的加权和是一个椭球体，在二维的特征值情况下，二阶导数是一个椭圆，椭圆方程为：
$$
y=\frac{\lambda_1}{\frac{1}{t_1^2}}+\frac{\lambda_2}{\frac{1}{t_2^2}}
$$
&lt;img src="https://img-blog.csdnimg.cn/img_convert/bb30779d25d486346799cb0fce7d34ad.png#pic_center" alt="在这里插入图片描述"&gt;&lt;/p&gt;
&lt;p&gt;由图可知，最大二阶导数由最大特征值决定（长半轴），而最小二阶导数由最小特征值决定（短半轴）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="海森矩阵应用"&gt;海森矩阵应用&lt;/h2&gt;
&lt;p&gt;在弄清海森矩阵的基本定义后，就可以使用海森矩阵的一些性质来分析优化方法中的一些问题了。如确定局部最大点、局部最小点和鞍点、确定学习率、以及使用病态条件来确定梯度下降的表现等，同时我们还可以利用Hessian矩阵来实现&lt;strong&gt;牛顿法&lt;/strong&gt;这种优化算法，。&lt;/p&gt;
&lt;p&gt;（本节完）&lt;/p&gt;

          </description>
    <category>机器学习</category><category>数学基础</category></item>
    <item>
      <title>探索</title>
      <link>https://tommycheese.github.io/gallery/</link>
      <pubDate>Sat, 25 Jun 2022 18:35:46 +0530</pubDate>
      <guid>https://tommycheese.github.io/gallery/</guid>
      <description></description>
    </item>
  </channel>
</rss>
