写在前面
最近我做了一个有点绕、但很有意思的项目:
我让 AI 和我一起,把 shareai-lab/learn-claude-code 改写成了一个面向前端工程师的 TypeScript 版本,并且用 代数效应(Algebraic Effects) 重新解释它背后的 Agent harness 结构。
先说清楚,这不是一个 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 工具。
如果你是前端工程师,可以把它粗略类比成:
| 前端里熟悉的东西 | 代数效应里的相似点 |
|---|---|
| 用户点击触发 event | Agent perform() 一个 effect |
| reducer 描述状态变化 | Agent 描述下一步意图 |
| middleware 拦截 action | handler / hook 拦截 effect |
| mock service worker 替换请求 | 测试 handler 替换真实副作用 |
这个类比虽不完全严谨,但足够建立直觉:重点不是“神秘语法”,而是把声明和解释分开。
为什么是 TypeScript
原版 Learn Claude Code 用 Python 写,当然没问题。Python 很适合快速表达 agent loop,也适合教学。
但我这次选择 TypeScript,有三个原因。
1. 便于前端学习
如果一个前端同学想理解 Claude Code 背后的 harness,用 TypeScript 能少跨一层语言心智。Effect、Handler、ToolDefinition、Message、TaskRecord 这些概念,一旦被 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() 做的事也很朴素:
- 让 Agent 往前走一步。
- 如果 Agent yield 出一个 effect,就找对应 handler。
- 执行 handler。
- 把结果送回 Agent。
- 重复,直到 Agent 结束。
这就是我觉得代数效应适合解释 Agent harness 的原因:它让“模型想做什么”和“系统允许它怎么做”之间多了一层清楚的工程边界。
20 章其实是在不断扩展同一个 harness
这个 TypeScript 版本保留了 Learn Claude Code 原版 20 章递进结构。
每一章不是孤立功能,而是在给 harness 增加一个新维度:
| 章节 | 主题 | Harness 新增能力 |
|---|---|---|
| s01 | Agent Loop | 让模型、工具、结果回灌形成闭环 |
| s02 | Tool Use | 把真实世界能力放进工具池 |
| s03 | Permission | 给危险副作用加边界 |
| s04 | Hooks | 在工具前后插入日志、审批、提醒 |
| s05 | TodoWrite | 把长任务计划外部化 |
| s06 | Subagent | 用独立上下文隔离探索噪声 |
| s07 | Skill Loading | 按需加载专业知识 |
| s08 | Context Compact | 在上下文有限时压缩历史 |
| s09 | Memory | 保存跨会话长期知识 |
| s10 | System Prompt | 根据运行时状态组装 prompt |
| s11 | Error Recovery | 分类处理错误和重试 |
| s12 | Task System | 用持久任务板管理长期工作 |
| s13 | Background Tasks | 让慢任务异步运行并回传通知 |
| s14 | Cron Scheduler | 让时间成为外部触发源 |
| s15 | Agent Teams | 给多 Agent 提供身份和邮箱 |
| s16 | Team Protocols | 把协作变成 request-response 协议 |
| s17 | Autonomous Agents | 让空闲 agent 自主认领任务 |
| s18 | Worktree Isolation | 给并行修改提供目录隔离 |
| s19 | MCP Plugin | 用标准协议接入外部工具 |
| s20 | Comprehensive | 把所有机制重新收束到一个 loop |
学完这 20 章后,我最大的感受是:复杂 Agent 不是靠“更多 prompt”堆出来的,而是靠一层一层 harness 机制支撑起来的。
仓库现在是两层结构
为了兼顾学习和工程化,我把仓库整理成两层。
第一层是主线课程:
s01_agent_loop/
s02_tool_use/
...
s20_comprehensive/
每章都有自己的 README.md 和 agent.ts。agent.ts 通常分成四段:
- Effect 类型定义
- Handler 实现
- Agent Generator
- 启动入口
这层代码有意保留重复,因为它服务的是“逐章学习”。打开一个章节,你不需要先理解一个大框架,就能看到完整闭环。
第二层是工程化参考:
src/core/
src/modules/
tests/
docs/
这里把重复出现的机制抽成共享模块,比如:
src/core/effects.ts:更通用的 Effect runtimesrc/core/agent.ts:最小 Agent loopsrc/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
...
每一章只问三个问题:
- 这一章新增了哪种 harness 能力?
- 这个能力在代码里表现为 Effect、Handler,还是 State?
- 如果接入真实模型 API,需要改 Agent 逻辑,还是只改 handler?
能一直回答这三个问题,就说明你是真的在理解 Agent harness。
最后
我越来越觉得,学习 AI coding 工具最好的方式,不只是使用它们,而是把它们背后的工程结构拆开。
这个项目对我来说,就是一次这样的拆解:用 AI 协作,把 github.com/shareAI-lab… 转成 TypeScript;再用代数效应,把工具调用、权限、记忆、任务、团队、MCP 这些机制放回同一个解释框架里。
希望它能帮更多前端工程师从“会用 AI 工具”往前走一步,开始理解“AI 工具为什么能这样工作”。