Algebraic Effects Learn Claude Code

61 阅读13分钟

写在前面

最近我做了一个有点绕、但很有意思的项目:

我让 AI 和我一起,把 shareai-lab/learn-claude-code 改写成了一个面向前端工程师的 TypeScript 版本,并且用 代数效应(Algebraic Effects) 重新解释它背后的 Agent harness 结构。

项目地址:github.com/m7yue/learn…

先说清楚,这不是一个 Claude Code 产品复刻,也不是又一个“从零实现 Coding Agent”的玩具项目。

它更像是一份可以运行的学习笔记:你可以配合原版教程 Learn Claude Code 理解原始问题,再回到仓库里看对应的 TypeScript 实现。Agent loop、工具调用、权限、hooks、todo、subagent、skill、context compact、memory、task、team、MCP 这些机制,都可以用 Effect(效应)Handler(处理器)State(状态) 的方式重新串起来。

这个项目对我来说最有意义的地方,不是把“代码从 Python 变成了 TypeScript”,而是我在做的过程中逐渐意识到:

我们使用 AI coding 工具时,看到的是智能;
但真正支撑它工作的,是一套 harness 工程。

为什么我会想做这件事

现在很多前端同学已经开始高频使用 Claude Code、Cursor、Codex 一类工具。刚开始用的时候,很容易被它们的“智能感”吸引:它能读代码,能改文件,能跑测试,能总结错误,甚至能自己拆任务。

但用久了以后,问题也会冒出来:

  • 为什么它有时能连续推进,有时却原地打转?
  • 为什么工具结果必须回到上下文里?
  • 为什么有的命令需要审批,有的命令可以直接跑?
  • 为什么上下文满了以后,agent 的表现会变差?
  • 为什么 Todo、Memory、Subagent、MCP 这些机制不是可有可无的装饰?

我真正想理解的不是“怎么调用某个模型 API”,而是:

一个 coding agent 到底是怎么被工程化地装起来的?

Learn Claude Code 原项目给了一个很好的路径。它从一个最小 loop 开始,每一章只加一个机制,最后扩展成完整的 Agent harness。

但作为前端工程师,相比 Python,我更希望用 TypeScript、类型系统和前端熟悉的工程抽象去重新理解它。

更巧的是,最近 coding agent 的工具链也在明显重视 TypeScript。MoonshotAI 的 Kimi Code 就是一个例子:官方仓库已经采用 TypeScript/Node 开发流,也有近期资讯提到它从早期 Python 形态转向 TypeScript/Bun 架构。

TypeScript 正在成为一类 agent harness 很自然的表达工具:类型适合描述工具 schema,前端生态适合做交互式 TUI,Node/Bun 适合贴近本地文件系统和命令行。

代数效应是什么

很多人看到“代数效应”四个字可能会立刻劝退,感觉这是编程语言理论里的东西。

前端同学其实已经在 React 中接触过代数效应。比如这个组件:组件不是直接操作一切,而是描述“我想要的 UI 和副作用”,再由 React runtime 统一调度、解释和提交。

function Profile() {
  const [name, setName] = useState("Ada");

  useEffect(() => {
    document.title = name;
  }, [name]);

  return <h1>{name}</h1>;
}

你没有在组件里手动创建 DOM,也没有自己决定 effect 的精确执行时机。组件描述 UI 和副作用,React 负责调度 render、commit、effect。

再看 Suspense:

<Suspense fallback={<Loading />}>
  <Profile />
</Suspense>

子树如果暂时“没准备好”,React 可以让外层边界接住这个状态,先显示 fallback,之后再继续。这和代数效应思想是回事,但它们共享一个很重要的直觉:业务代码可以声明需求,外层 runtime 负责接住、解释、恢复。

所以如果你写过 React,不必把代数效应当成一个完全陌生的东西。你可以先把它理解成一种更通用的 runtime 边界设计:组件/Agent 描述意图,runtime 解释意图。

如果只为了理解这个项目,可以先不用管代数效应的完整理论。先记住一句话:

代数效应 = 业务逻辑只声明“我要做什么”,运行时再决定“怎么做”。

举个普通的例子,如果 Agent 要执行一个 shell 命令,最直接的代码可能是:

const output = await exec(command);

这当然能跑,但问题是 Agent 逻辑和真实副作用绑死了。以后你想加权限审批、日志、mock、沙盒、重试,都要围着这行代码改。

换成效应写法,可以变成:

const output = yield* perform({
  type: "BashExec",
  input: { command },
});

这段代码没有直接执行 shell。它只是声明:我需要一个 BashExec 效应。

真正怎么执行,由外部 handler 决定:

const handlers = {
  BashExec: async (effect) => {
    return exec(effect.input.command);
  },
};

这就是它最关键的地方:Agent 逻辑表达意图,handler 解释意图。

换一套 handler,就可以让同一个 Agent 可以在不同环境里运行:

  • 教学环境:返回固定字符串,不真的执行命令。
  • 测试环境:记录调用,断言参数。
  • 安全环境:先过权限规则,再决定是否执行。
  • 真实环境:调用 shell、文件系统、模型 API 或 MCP 工具。

如果你是前端工程师,可以把它粗略类比成:

前端里熟悉的东西代数效应里的相似点
用户点击触发 eventAgent perform() 一个 effect
reducer 描述状态变化Agent 描述下一步意图
middleware 拦截 actionhandler / hook 拦截 effect
mock service worker 替换请求测试 handler 替换真实副作用

这个类比虽不完全严谨,但足够建立直觉:重点不是“神秘语法”,而是把声明和解释分开。

为什么是 TypeScript

原版 Learn Claude Code 用 Python 写,当然没问题。Python 很适合快速表达 agent loop,也适合教学。

但我这次选择 TypeScript,有三个原因。

1. 便于前端学习

如果一个前端同学想理解 Claude Code 背后的 harness,用 TypeScript 能少跨一层语言心智。EffectHandlerToolDefinitionMessageTaskRecord 这些概念,一旦被 TypeScript 类型写出来,就会比散落在动态对象里更清楚。

2. Agent harness 天然需要类型边界

一个工具有什么参数、返回什么结果、是否需要审批、能否被 mock、是否能进入 MCP 工具池,这些都适合用类型约束。类型不是为了炫技,而是为了让 harness 的边界更可检查。

3. 行业里的 coding agent 工具也在靠近 TS 生态。

Kimi Code 是一个很好的信号。TypeScript 正在成为 agent 工具链里越来越重要的工程选择。

这和该项目的方向刚好形成呼应:如果 coding agent 越来越像一个本地交互式工程工具,那么 TypeScript 不只是“前端语言”,它也很适合表达 agent 的工具 schema、状态机、TUI、插件和 runtime 边界。

Agent 的本质不是“会调用工具的聊天机器人”

我现在更愿意用这个公式理解 coding agent:
Agent = 模型的 agency + harness
Harness = loop + tools + permissions + context + memory + state + protocols

模型负责产生意图,比如:

  • 我要读文件。
  • 我要运行测试。
  • 我要修改代码。
  • 我要拆一个子任务。
  • 我要查长期记忆。

Harness 负责把这些意图解释成真实世界里的动作:

  • 哪些工具可以用?
  • 哪些动作需要审批?
  • 工具结果如何回灌给模型?
  • 上下文满了怎么办?
  • 长期记忆放在哪里?
  • 多个 agent 如何通信?
  • 外部工具如何通过 MCP 接入?

所以工程师真正能设计的,不是模型本身的 agency,而是模型外面的 harness。

这也是 Learn Claude Code 值得学习的地方。它不是教你“调一个 API”,而是在展示一个 coding agent 的 harness 是怎样一步步长出来的。

原版 loop 用代数效应看,会突然变清楚

原版 Python 课程里的核心循环,大概长这样:

while True:
    response = client.messages.create(...)
    if response.stop_reason != "tool_use":
        return
    for block in response.content:
        output = TOOL_HANDLERS[block.name](**block.input)
        results.append(output)
    messages.append(results)

这段代码看起来是在“循环调用模型和工具”。但从代数效应视角看,它其实可以拆成三个角色:

角色作用
Agent logic决定下一步要做什么
Effect把“要做什么”表达成纯数据
Handler把这个意图解释成真实副作用

对应到 TypeScript 里就是:

function* agentLoop(perform: PerformFn<MyEffect>) {
  const response = yield* perform({
    type: "Chat",
    input: { messages },
  });

  const output = yield* perform({
    type: "BashExec",
    input: { command: "ls" },
  });

  return output;
}

这个 Generator 里的代码像同步代码一样好读,但每一次 yield* perform(...) 都是在把一个意图交给运行时。

运行时 runAgent() 做的事也很朴素:

  1. 让 Agent 往前走一步。
  2. 如果 Agent yield 出一个 effect,就找对应 handler。
  3. 执行 handler。
  4. 把结果送回 Agent。
  5. 重复,直到 Agent 结束。

这就是我觉得代数效应适合解释 Agent harness 的原因:它让“模型想做什么”和“系统允许它怎么做”之间多了一层清楚的工程边界。

20 章其实是在不断扩展同一个 harness

这个 TypeScript 版本保留了 Learn Claude Code 原版 20 章递进结构。

每一章不是孤立功能,而是在给 harness 增加一个新维度:

章节主题Harness 新增能力
s01Agent Loop让模型、工具、结果回灌形成闭环
s02Tool Use把真实世界能力放进工具池
s03Permission给危险副作用加边界
s04Hooks在工具前后插入日志、审批、提醒
s05TodoWrite把长任务计划外部化
s06Subagent用独立上下文隔离探索噪声
s07Skill Loading按需加载专业知识
s08Context Compact在上下文有限时压缩历史
s09Memory保存跨会话长期知识
s10System Prompt根据运行时状态组装 prompt
s11Error Recovery分类处理错误和重试
s12Task System用持久任务板管理长期工作
s13Background Tasks让慢任务异步运行并回传通知
s14Cron Scheduler让时间成为外部触发源
s15Agent Teams给多 Agent 提供身份和邮箱
s16Team Protocols把协作变成 request-response 协议
s17Autonomous Agents让空闲 agent 自主认领任务
s18Worktree Isolation给并行修改提供目录隔离
s19MCP Plugin用标准协议接入外部工具
s20Comprehensive把所有机制重新收束到一个 loop

学完这 20 章后,我最大的感受是:复杂 Agent 不是靠“更多 prompt”堆出来的,而是靠一层一层 harness 机制支撑起来的。

仓库现在是两层结构

为了兼顾学习和工程化,我把仓库整理成两层。

第一层是主线课程:

s01_agent_loop/
s02_tool_use/
...
s20_comprehensive/

每章都有自己的 README.mdagent.tsagent.ts 通常分成四段:

  1. Effect 类型定义
  2. Handler 实现
  3. Agent Generator
  4. 启动入口

这层代码有意保留重复,因为它服务的是“逐章学习”。打开一个章节,你不需要先理解一个大框架,就能看到完整闭环。

第二层是工程化参考:

src/core/
src/modules/
tests/
docs/

这里把重复出现的机制抽成共享模块,比如:

  • src/core/effects.ts:更通用的 Effect runtime
  • src/core/agent.ts:最小 Agent loop
  • src/modules/tools.ts:工具池
  • src/modules/permission.ts:权限管线
  • src/modules/todos.ts:Todo 状态
  • src/modules/memory.ts:长期记忆
  • src/modules/tasks.ts:任务控制面
  • src/modules/team.ts:Agent 团队通信
  • src/modules/mcp.ts:MCP 工具池

这两层不是两套互相竞争的实现。

主线章节负责把问题讲清楚;工程化模块负责展示这些问题在真实 TypeScript 项目里可以如何组织。先读章节,再看模块,会顺很多。

我为什么强调这是 AI 协作完成的

因为这件事本身就是我想分享的一部分。

以前学习一个开源项目,通常是读源码、做笔记、写 demo。现在多了一种方式:让 AI 帮你做跨语言转写、结构对照、抽象重述,然后你再反过来审查它、修正它、组织它。

这个过程中,人不能退化成“验收 AI 输出的人”。真正关键的是:

  • 你要知道自己想学什么。
  • 你要能判断 AI 给出的抽象是否成立。
  • 你要能发现机械转换里丢掉的语境。
  • 你要能把生成内容重新组织成面向人的学习路径。

这次项目里我就踩到了一个很典型的坑:AI 很容易把另一个仓库里的目录和文档直接搬过来,看起来很完整,但放到当前项目里会产生叙事错位。

所以 AI 协作不是“全自动生成一个项目”,而是人和 AI 一起把理解往前推。AI 很快,但你必须负责方向、取舍和最后的连贯性。

前端工程师可以从这里学到什么

如果你是前端工程师,我觉得这个项目至少有三个学习价值。

1. TypeScript 能帮你看清 harness 边界。

Effect 的 input/output、handler map、message 类型、task record,这些东西一旦用类型表达出来,很多原本模糊的机制会变得具体。

2. 代数效应提供了一种很适合 Agent 的控制流模型。

Agent 逻辑负责声明意图,运行时负责解释副作用。这和前端里的事件系统、状态管理、middleware、mock 网络层都有相通之处。

3. 你会更容易理解现在的 coding agent 工具。

当你知道 Permission、Hooks、Todo、Compact、Memory、Subagent、MCP 分别解决什么问题,再去看 Claude Code、Codex 或 Cursor 的行为,会更容易判断它们背后的工程取舍。

怎么开始

如果你想看这个项目,建议不要一上来就研究所有模块。先结合 Learn Claude Code 中文站 对照学习:先读原站对应章节理解动机,再回到本仓库看 TypeScript + Algebraic Effects 版本。

先跑起来:

npm install
npm run s01
npm run s02
npm run s20

然后按这个顺序读:

README.md
docs/algebraic-effects-primer.md
docs/learn-shareai-crosswalk.md
s01_agent_loop/README.md
s01_agent_loop/agent.ts
s02_tool_use/README.md
s02_tool_use/agent.ts
...

每一章只问三个问题:

  1. 这一章新增了哪种 harness 能力?
  2. 这个能力在代码里表现为 Effect、Handler,还是 State?
  3. 如果接入真实模型 API,需要改 Agent 逻辑,还是只改 handler?

能一直回答这三个问题,就说明你是真的在理解 Agent harness。

最后

我越来越觉得,学习 AI coding 工具最好的方式,不只是使用它们,而是把它们背后的工程结构拆开。

这个项目对我来说,就是一次这样的拆解:用 AI 协作,把 github.com/shareAI-lab… 转成 TypeScript;再用代数效应,把工具调用、权限、记忆、任务、团队、MCP 这些机制放回同一个解释框架里。

希望它能帮更多前端工程师从“会用 AI 工具”往前走一步,开始理解“AI 工具为什么能这样工作”。

👉 github.com/m7yue/learn…