普通视图
南向资金净买入额超110亿港元
龙湖完成两笔公司债本息兑付,合计1.7亿元
Everything Claude Code 文档
一、项目概述
Everything Claude Code(ECC) 是一个 AI Agent 工作框架性能优化系统,由 Anthropic Hackathon 获奖者 Affaan Mustafa 开发,历经 10 个月以上的生产环境实战积累。
它不只是配置文件的集合,而是一套完整的系统,包含:
- Skills(技能):可复用的工作流定义
- Instincts(直觉):持续学习,自动从会话中提取模式
- Memory Optimization(记忆优化):跨会话上下文持久化
- Security Scanning(安全扫描):AgentShield 集成,102 条规则
- Research-first Development(研究优先开发):先调研再编码
支持平台:Claude Code、Codex CLI、Cursor IDE、OpenCode、Cowork 及其他 AI Agent 工作框架。
二、核心架构
目录结构总览
everything-claude-code/
├── .claude-plugin/ # 插件和市场清单(plugin.json, marketplace.json)
├── agents/ # 13 个专业子代理(.md 文件)
├── .agents/skills/ # 技能定义(SKILL.md + openai.yaml)
├── commands/ # 32 个斜杠命令(.md 文件)
├── rules/ # 编码规范(common/ + typescript/ + python/ + golang/)
├── hooks/ # 触发式自动化(hooks.json + 脚本)
├── scripts/ # 跨平台 Node.js 脚本
├── contexts/ # 动态系统提示注入上下文
├── examples/ # CLAUDE.md 配置示例(Next.js、Go、Django 等)
├── mcp-configs/ # MCP 服务器配置(GitHub、Supabase、Vercel 等)
├── .cursor/ # Cursor IDE 适配层
├── .codex/ # Codex CLI 适配层
├── .opencode/ # OpenCode 适配层
└── docs/ # 完整文档
三、快速开始(2 分钟上手)
步骤一:安装插件(推荐)
# 添加为市场源
/plugin marketplace add affaan-m/everything-claude-code
# 安装插件
/plugin install everything-claude-code@everything-claude-code
或者直接在 ~/.claude/settings.json 中添加:
{
"extraKnownMarketplaces": {
"everything-claude-code": {
"source": {
"source": "github",
"repo": "affaan-m/everything-claude-code"
}
}
},
"enabledPlugins": {
"everything-claude-code@everything-claude-code": true
}
}
步骤二:安装规则(必须手动)
⚠️ Claude Code 插件系统不支持自动分发规则文件,需手动安装。
git clone https://github.com/affaan-m/everything-claude-code.git
cd everything-claude-code
# 使用安装脚本(推荐)
./install.sh typescript # TypeScript 项目
./install.sh python # Python 项目
./install.sh typescript python # 多语言项目
# 针对 Cursor
./install.sh --target cursor typescript
步骤三:开始使用
# 规划新功能
/everything-claude-code:plan "Add user authentication"
# 查看所有可用命令
/plugin list everything-claude-code@everything-claude-code
安装完成后你即可使用:13 个 Agents + 56 个 Skills + 32 个 Commands。
四、核心模块详解
4.1 Agents(专业子代理)—— 13 个
| Agent | 描述 |
|---|---|
planner |
功能实现规划 |
architect |
系统设计决策 |
tdd-guide |
测试驱动开发指导 |
code-reviewer |
代码质量与安全审查 |
security-reviewer |
漏洞分析(OWASP Top 10) |
build-error-resolver |
构建错误修复 |
e2e-runner |
Playwright E2E 测试 |
refactor-cleaner |
死代码清理 |
doc-updater |
文档同步更新 |
go-reviewer |
Go 代码审查 |
go-build-resolver |
Go 构建错误修复 |
python-reviewer |
Python 代码审查 |
database-reviewer |
数据库/Supabase 审查 |
调用示例:
/everything-claude-code:plan "Add OAuth authentication" # 触发 planner
/code-review # 触发 code-reviewer
/security-scan # 触发 security-reviewer
4.2 Skills(技能库)—— 56+ 个
Skills 是可复用的工作流定义,按领域分类:
通用开发:
-
coding-standards— 语言最佳实践 -
tdd-workflow— TDD 方法论(先写测试,80% 覆盖率) -
security-review— 安全检查清单 -
eval-harness/verification-loop— 验证循环评估 -
search-first— 先调研再编码工作流 -
api-design— REST API 设计与分页、错误响应 -
deployment-patterns— CI/CD、Docker、健康检查、回滚
前端:
-
frontend-patterns— React、Next.js 模式 -
frontend-slides— 零依赖 HTML 演示文稿(含 PPTX 转换) -
e2e-testing— Playwright E2E 测试 + Page Object Model
后端 & 数据库:
-
backend-patterns— API、数据库、缓存模式 -
clickhouse-io— ClickHouse 分析查询 -
postgres-patterns— PostgreSQL 优化 -
database-migrations— Prisma、Drizzle、Django、Go 迁移 -
docker-patterns— Docker Compose、网络、卷、容器安全
Python / Django:
-
django-patterns/django-security/django-tdd/django-verification -
python-patterns/python-testing
Java Spring Boot:
-
springboot-patterns/springboot-security/springboot-tdd/springboot-verification -
java-coding-standards/jpa-patterns
Go:
-
golang-patterns/golang-testing
Swift / iOS:
-
swift-actor-persistence— 基于 Actor 的线程安全数据持久化 -
swift-protocol-di-testing— 协议依赖注入可测试 Swift 代码 -
liquid-glass-design— iOS 26 Liquid Glass 设计系统 -
foundation-models-on-device— Apple 设备端 LLM -
swift-concurrency-6-2— Swift 6.2 并发特性
C++:
-
cpp-coding-standards— C++ Core Guidelines -
cpp-testing— GoogleTest + CMake/CTest
学习与优化:
-
continuous-learning/continuous-learning-v2— 自动从会话提取模式(含置信度评分) -
strategic-compact— 手动压缩建议 -
cost-aware-llm-pipeline— LLM 成本优化、模型路由、预算追踪 -
iterative-retrieval— 子代理的渐进式上下文精炼
内容创作(新增):
-
article-writing— 无 AI 腔调的长文写作 -
content-engine— 多平台社交内容工作流 -
market-research— 带来源标注的市场研究 -
investor-materials— 融资 Pitch Deck、备忘录、财务模型 -
investor-outreach— 个性化融资外联与跟进
4.3 Commands(斜杠命令)—— 32 个
开发流程:
| 命令 | 用途 |
|---|---|
/plan "..." |
创建功能实现计划 |
/tdd |
强制执行 TDD 工作流 |
/code-review |
审查代码变更 |
/build-fix |
修复构建错误 |
/e2e |
生成 E2E 测试 |
/refactor-clean |
清除死代码 |
/security-scan |
安全漏洞扫描 |
/test-coverage |
测试覆盖率分析 |
Go 专项:
| 命令 | 用途 |
|---|---|
/go-review |
Go 代码审查 |
/go-test |
Go TDD 工作流 |
/go-build |
修复 Go 构建错误 |
多代理编排:
| 命令 | 用途 |
|---|---|
/multi-plan |
多代理任务分解 |
/multi-execute |
编排多代理工作流 |
/multi-backend |
后端多服务编排 |
/multi-frontend |
前端多服务编排 |
/multi-workflow |
通用多服务工作流 |
/pm2 |
PM2 服务生命周期管理 |
/orchestrate |
多代理协调 |
持续学习系统:
| 命令 | 用途 |
|---|---|
/learn |
会话中提取模式 |
/learn-eval |
提取、评估并保存模式 |
/instinct-status |
查看已学习的直觉(含置信度) |
/instinct-import <file> |
导入他人直觉 |
/instinct-export |
导出直觉供分享 |
/evolve |
将相关直觉聚类为技能 |
其他实用命令:
| 命令 | 用途 |
|---|---|
/checkpoint |
保存验证状态 |
/verify |
运行验证循环 |
/eval |
按标准评估 |
/sessions |
会话历史管理 |
/update-docs |
更新文档 |
/skill-create |
从 Git 历史生成技能 |
/setup-pm |
配置包管理器 |
4.4 Hooks(触发式自动化)
Hooks 在工具事件发生时自动触发,示例:
{
"matcher": "tool == \"Edit\" && tool_input.file_path matches \"\\.(ts|tsx|js|jsx)$\"",
"hooks": [{
"type": "command",
"command": "grep -n 'console\\.log' \"$file_path\" && echo '[Hook] Remove console.log' >&2"
}]
}
内置 Hook 脚本(Node.js,全平台兼容):
-
session-start.js— 会话开始时自动加载上下文 -
session-end.js— 会话结束时自动保存状态 -
pre-compact.js— 压缩前保存状态 -
suggest-compact.js— 建议压缩时机 -
evaluate-session.js— 从会话中提取模式
4.5 Rules(编码规范)
按语言分目录组织,安装时按需选择:
rules/
├── common/ # 通用原则(必装)
│ ├── coding-style.md # 不可变性、文件组织
│ ├── git-workflow.md # Commit 格式、PR 流程
│ ├── testing.md # TDD、80% 覆盖率要求
│ ├── performance.md # 模型选择、上下文管理
│ ├── patterns.md # 设计模式
│ ├── hooks.md # Hook 架构
│ ├── agents.md # 子代理委托时机
│ └── security.md # 强制安全检查
├── typescript/ # TypeScript/JavaScript 专项
├── python/ # Python 专项
└── golang/ # Go 专项
五、生态工具
5.1 AgentShield — 安全审计工具
在 Anthropic x Cerebral Valley Hackathon(2026年2月) 上构建,1282 个测试,98% 覆盖率,102 条静态分析规则。
# 快速扫描(无需安装)
npx ecc-agentshield scan
# 自动修复安全问题
npx ecc-agentshield scan --fix
# 深度分析(三个 Opus 4.6 代理:红队/蓝队/审计员)
npx ecc-agentshield scan --opus --stream
# 从零生成安全配置
npx ecc-agentshield init
扫描范围: CLAUDE.md、settings.json、MCP 配置、hooks、agent 定义、skills,覆盖 5 大类别:
- 密钥检测(14 种模式)
- 权限审计
- Hook 注入分析
- MCP 服务器风险评估
- Agent 配置审查
输出格式: 终端彩色(A-F 评级)、JSON(CI 管道)、Markdown、HTML。
--opus模式会运行三个 Claude Opus 4.6 代理组成红队/蓝队/审计员流水线:攻击者寻找漏洞链,防御者评估保护,审计员综合出优先级风险报告。这是对抗性推理,而非单纯的模式匹配。
在 Claude Code 中直接运行:/security-scan
5.2 Skill Creator — 技能生成器
方式 A:本地分析(内置)
/skill-create # 分析当前仓库
/skill-create --instincts # 同时生成直觉
方式 B:GitHub App(高级)
适用于 10k+ commits、自动 PR、团队共享:
- 安装:github.com/marketplace…
- 在任意 Issue 中评论:
/skill-creator analyze - 支持 push 到主分支时自动触发
5.3 持续学习系统 v2
/instinct-status # 查看已学习的直觉及置信度
/instinct-import # 导入他人的直觉
/instinct-export # 导出自己的直觉分享给团队
/evolve # 将相关直觉聚类成可复用技能
六、多平台支持详情
6.1 各平台功能对比
| 功能 | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|---|---|---|---|---|
| Agents | ✅ 13 个 | 共享(AGENTS.md) | 共享 | ✅ 12 个 |
| Commands | ✅ 33 个 | 共享 | 指令式 | ✅ 24 个 |
| Skills | ✅ 50+ | 共享 | 10 个 | ✅ 37 个 |
| Hook 事件数 | 8 种 | 15 种 | ❌ 暂不支持 | 11 种 |
| Rules | ✅ 29 条 | ✅ 29 条(YAML) | 指令式 | 13 条 |
| MCP 服务器 | ✅ 14 个 | 共享 | 4 个 | 完整 |
| 自定义工具 | 通过 Hook | 通过 Hook | ❌ | ✅ 6 个原生 |
6.2 Cursor IDE 快速接入
./install.sh --target cursor typescript
./install.sh --target cursor python golang swift
Cursor 的 Hook 使用 DRY 适配器模式,adapter.js 将 Cursor 的 stdin JSON 转换为 Claude Code 格式,复用现有脚本无需重写。
关键 Hook:
-
beforeShellExecution— 阻止在 tmux 外启动开发服务器,审查git push -
afterFileEdit— 自动格式化 + TypeScript 检查 + console.log 警告 -
beforeSubmitPrompt— 检测提示词中的密钥(sk-、ghp_、AKIA等) -
beforeTabFileRead— 阻止读取.env、.key、.pem文件
6.3 Codex CLI 快速接入
cp .codex/config.toml ~/.codex/config.toml
codex # AGENTS.md 自动被检测
⚠️ Codex CLI 暂不支持 Hooks(GitHub Issue #2109,430+ 赞),安全策略通过
persistent_instructions和沙箱权限系统实现。
6.4 OpenCode 快速接入
npm install -g opencode
opencode # 从仓库根目录运行,自动检测 .opencode/opencode.json
OpenCode 的插件系统比 Claude Code 更强大,支持 20+ 事件类型(包括 file.edited、message.updated、lsp.client.diagnostics 等)。
七、Token 优化指南
推荐配置(加入 ~/.claude/settings.json)
{
"model": "sonnet",
"env": {
"MAX_THINKING_TOKENS": "10000",
"CLAUDE_AUTOCOMPACT_PCT_OVERRIDE": "50",
"CLAUDE_CODE_SUBAGENT_MODEL": "haiku"
}
}
| 设置 | 默认值 | 推荐值 | 效果 |
|---|---|---|---|
model |
opus | sonnet | 约降低 60% 成本 |
MAX_THINKING_TOKENS |
31,999 | 10,000 | 约降低 70% 隐藏推理成本 |
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE |
95 | 50 | 更早压缩,长会话质量更好 |
日常工作流命令
| 命令 | 使用时机 |
|---|---|
/model sonnet |
默认,适用于大多数任务 |
/model opus |
复杂架构设计、深度调试 |
/clear |
切换到无关联任务时(免费、即时重置) |
/compact |
完成里程碑后、开始下一任务前 |
/cost |
监控会话中的 Token 消耗 |
上下文窗口管理
⚠️ 不要同时启用所有 MCP 服务器。 每个 MCP 工具描述都会消耗 200k 上下文窗口中的 Token,可能将可用上下文压缩到 ~70k。
// 在项目 .claude/settings.json 中禁用不需要的 MCP
{
"disabledMcpServers": ["supabase", "railway", "vercel"]
}
建议:每个项目启用 不超过 10 个 MCP,活跃工具不超过 80 个。
八、典型工作流示例
开发新功能
# 1. 规划
/everything-claude-code:plan "Add user authentication with OAuth"
# → planner 代理创建实现蓝图
# 2. 测试驱动开发
/tdd
# → tdd-guide 强制先写测试
# 3. 代码审查
/code-review
# → code-reviewer 检查回归问题
修复 Bug
# 1. 写一个能复现 bug 的失败测试
/tdd
# → tdd-guide:先写失败测试
# 2. 实现修复,验证测试通过
# 3. 审查
/code-review
# → code-reviewer:捕获潜在回归
上线前检查
/security-scan # → 安全漏洞审计(OWASP Top 10)
/e2e # → 关键用户流程 E2E 测试
/test-coverage # → 验证 80%+ 覆盖率
代理选择速查表
| 我想要... | 使用命令 | 调用代理 |
|---|---|---|
| 规划新功能 | /plan "Add auth" |
planner |
| 系统架构设计 |
/plan + 架构师模式 |
architect |
| TDD 写代码 | /tdd |
tdd-guide |
| 审查刚写的代码 | /code-review |
code-reviewer |
| 修复失败构建 | /build-fix |
build-error-resolver |
| E2E 测试 | /e2e |
e2e-runner |
| 安全漏洞扫描 | /security-scan |
security-reviewer |
| 清理死代码 | /refactor-clean |
refactor-cleaner |
| 更新文档 | /update-docs |
doc-updater |
| 审查 Go 代码 | /go-review |
go-reviewer |
| 审查 Python 代码 | /python-review |
python-reviewer |
九、版本更新历史
v1.7.0(2026年2月)— 跨平台扩展 + 演示文稿构建器
- Codex app + CLI 支持 — 基于 AGENTS.md 的直接 Codex 支持
- frontend-slides 技能 — 零依赖 HTML 演示文稿构建器,含 PPTX 转换指导
-
5 个新通用业务技能 —
article-writing、content-engine、market-research、investor-materials、investor-outreach - 更广泛的工具覆盖 — Cursor、Codex 和 OpenCode 支持更完善
- 992 内部测试 — 扩展了插件、Hooks、技能和打包的验证覆盖
v1.6.0(2026年2月)— Codex CLI、AgentShield 与市场
-
Codex CLI 支持 — 新增
/codex-setup命令生成codex.md -
7 个新技能 —
search-first、swift-actor-persistence、swift-protocol-di-testing、regex-vs-llm-structured-text等 -
AgentShield 集成 —
/security-scan可直接运行 AgentShield(1282 测试,102 规则) - GitHub Marketplace — ECC Tools GitHub App 上线,含免费/专业/企业层级
- 30+ 社区 PR 合并
v1.4.0(2026年2月)— 多语言规则、安装向导 & PM2
-
交互式安装向导 —
configure-ecc技能提供引导式设置 - PM2 & 多代理编排 — 6 个新命令
-
多语言规则架构 —
common/+typescript/+python/+golang/ - 中文(zh-CN)翻译 — 80+ 文件完整翻译
- GitHub Sponsors 支持
v1.3.0(2026年2月)— OpenCode 插件支持
- 完整 OpenCode 集成 — 12 个代理、24 个命令、16 个技能(含 Hook 支持)
-
3 个原生自定义工具 —
run-tests、check-coverage、security-audit -
LLM 文档 —
llms.txt提供完整 OpenCode 文档
v1.2.0(2026年2月)— 统一命令 & 技能
- Python/Django 支持 — Django 模式、安全、TDD、验证技能
- Java Spring Boot 技能 — 模式、安全、TDD、验证
-
会话管理 —
/sessions命令 - 持续学习 v2 — 基于直觉的学习,含置信度评分
十、系统要求
- Claude Code CLI:最低版本 v2.1.0+
-
检查版本:
claude --version
⚠️ 贡献者注意:不要在
.claude-plugin/plugin.json中添加"hooks"字段。Claude Code v2.1+ 会从已安装插件自动加载hooks/hooks.json,显式声明会导致重复检测错误(详见 Issue #29、#52、#103)。
十一、常见问题
Q:如何查看已安装的 agents/commands?
/plugin list everything-claude-code@everything-claude-code
Q:Hooks 不工作 / 出现"Duplicate hooks file"错误?
检查 .claude-plugin/plugin.json 中是否有 "hooks" 字段,有则删除。
Q:上下文窗口迅速缩小? 禁用未使用的 MCP 服务器(见第七节),保持活跃 MCP 不超过 10 个。
Q:可以只使用部分组件吗? 可以,使用手动安装方式(Option 2),按需复制文件。每个组件完全独立。
Q:如何贡献新技能?
Fork 仓库 → 在 skills/your-skill-name/SKILL.md 创建技能(含 YAML frontmatter)→ 提交 PR。
Nextjs ISR 企业落地实战
背景
Nextjs 项目本来使用的是 SSG 来渲染门户网站的 blog 的,目录如下:
![]()
这样技术实现是简单,但是维护成本比较高。运营和销售同学写完营销文章后,需要推送给研发,由研发录入到 git 仓库中,并且执行一遍发布流程。这样做,没有办法将文章撰写作为一个独立的营销任务,必须借助技术发布。而且还有个问题,blog 越来越多,放在仓库里,会导致仓库大小越来越大:
![]()
需求与技术方案设计
于是我们就设计了一个这样的内部需求:
维护一个内部的 blog 发布平台,运营录入后点击发布,同时通知 Nextjs 项目触发更新文章。
技术方案:
- 内部 blog 发布平台使用 antd + go 搭建,负责录入文章到数据库,并提供公网接口获取
- Nextjs 改造为通过接口获取动态数据,设置缓存来优化访问;并暴露 API,内部 blog 发布平台触发更新后清除缓存,用户下次访问就回去拉取最新的数据源。
初步实现
内部 blog 发布平台
没啥说的,普通的后台管理系统,如图
![]()
md 编辑器就使用掘金的 bytemd
![]()
用户录入后,存入到数据库中,并暴露接口来获取。
Nextjs 项目改造
页面配置:
export const dynamic = 'auto'; // 允许页面缓存
export const dynamicParams = true;
export const revalidate = 259200; // 页面缓存 3 天(与 fetch 缓存时间一致)
将读取静态目录换为通过接口(自己开发 API 提供数据源)获取:
// SSR 页面
const postData = {
Action: "GetPublishedArticleList",
Lang: lng,
};
const startTime = Date.now();
const res = await fetch(blogApiUrl, {
method: "POST",
headers: {
'remote_user': 'admin',
'Content-Type': 'application/json',
},
body: JSON.stringify(postData),
// 设置缓存:有效期3天(259200秒),使用标签以便外部API可以清除缓存
next: {
revalidate: 259200, // 3天
tags: [`blog-list-${lng}`],
},
});
// 拿到数据后处理
if (res.ok) {
const response = await res.json();
const count = response?.Data?.Total || 0;
...
}
...
然后暴露 API,负责清除 fetch 缓存:
![]()
该 API 要记得配置 cors 跨域和来源 ip 和频次限制。
此外,需要配置 api 请求拦截,避免被中间件等影响造成请求不到地址:
async headers() {
return [
{
// 排除 /api 路径,让 API 路由自己处理 CORS
source: "/((?!api).)*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "*", // Set your origin
},
{
key: "Access-Control-Allow-Methods",
value: "GET, POST, PUT, DELETE, OPTIONS",
},
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
],
},
];
},
ISR 工作流程
首次访问:
- 服务端渲染(SSR), fetch 缓存
- 生成 HTML 并缓存
- 返回给用户
后续访问(3 天内):
- 直接返回缓存的 HTML, 不重新渲染, 响应快
3 天后:
- 第一个请求触发后台重新渲染
- 更新全部缓存
- 后续请求使用新缓存
调用 /api/revalidate-blog:
- 立即清除缓存
- 下次访问重新渲染
落地演示
新建一篇文章,点击发布,更新状态:
![]()
调用 revalidate API 触发缓存更新:
![]()
线上刷新查看:
![]()
成功!!
现货黄金站上5400美元/盎司
「完全理解」1 分钟实现自己的 Coding Agent
背景
![]()
相信大家都已经体验过了市面上各种 Coding Agent 应用(Claude Code、codeX、kimi-cli...),一定也对其原理有过好奇。废话不多说,咱们今天直实现一个 Mini Coding Agent
阅读本文前最好对 llm、tools 有一些基础的理解,(可以阅读笔者之前的文章「完全理解」MCP 到底是什么?从零开始实现一个完整的 MCP 调用链)
本文案例代码:github.com/qqqqqcy/cod…
1. 1 分钟实现 Mini Coding Agent
实现一个 Mini Coding Agent 其实并不复杂,跟着步骤来甚至都花不了 1 分钟。我们先给自己的项目取个名称方便后续称呼,为了省事咱们就叫 mca(Mini Coding Agent)
1.1. 快速实现
在根目录 mca 中初始化 package.json 文件和对应目录,因为是个分阶段的项目,第一个阶段先新建一个 0_deepagents 文件夹
mca
├── .env
├── 0_deepagents
│ └── src
│ ├── index.js
│ └── test.js
└── package.json
项目主要目的是弄懂 Coding Agent 原理,所以我们不会从 LLM 调用开始,会基于成熟的 SDK 封装。作为一个前端,可以先非常粗略的类比一了下后续会用到的相关依赖,有一个基础概念
| 基础层级 | 成熟封装的 SDK | 成熟应用(配置好了路由、全局 store、组件库、项目基础框架) |
|---|---|---|
| JavaScript | Vue、React | 各种开箱即用的基础应用 |
| LLM API | LangChain、LangGraph | Deep Agents |
首先我们基于 deepagents 来实现 mca,同时过程中会涉及环境变量,所有先以下三个依赖
npm i @langchain/deepseek deepagents dotenv
deepagents是一个独立的库,用于构建能够处理复杂多步骤任务的智能体。它基于 LangGraph 构建,并受到 Claude Code、Deep Research 和 Manus 等应用的启发,具备规划能力、用于上下文管理的文件系统以及生成子代理的能力。
{
"name": "mini-coding-agent",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"start": "node ./0_deepagents/src/index.js"
},
"license": "ISC",
"dependencies": {
"@langchain/deepseek": "^1.0.9",
"deepagents": "^1.7.0",
"dotenv": "^17.2.4"
}
}
先不说废话,直接基于 deepagents 创建一个智能体。
如果使用的非 deepseek model 可以参考此处替换对应依赖
// file: 0_deepagents/src/index.js
import "dotenv/config";
import { ChatDeepSeek } from "@langchain/deepseek";
import { createDeepAgent, FilesystemBackend } from "deepagents";
// 项目根目录,用于提示词中告知 Agent 工作目录
const PROJECT_ROOT = process.cwd();
// 简洁的 ReAct 风格系统提示词
const CODING_AGENT_SYSTEM_PROMPT = `---
PROJECT_ROOT: ${PROJECT_ROOT}
---
// As a ReAct coding agent, interpret user instructions and execute them using the most suitable tool.
`;
// 创建 Coding Agent 所需的全部代码 ⬇️⬇️⬇️
export const codingAgent = createDeepAgent({
// 声明 agent 所用模型
model: new ChatDeepSeek({
model: "deepseek-chat",
temperature: 0,
maxTokens: 4096,
apiKey: process.env.DEEPSEEK_API_KEY,
}),
// createDeepAgent 会使用虚拟容器,因此我们指定一下位置为命令行执行目录
backend: new FilesystemBackend({
rootDir: PROJECT_ROOT,
virtualMode: true
}),
systemPrompt: CODING_AGENT_SYSTEM_PROMPT,
name: "coding_agent",
});
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('请输入指令');
process.exit(1);
} else {
codingAgent.invoke({
messages: [{ role: "user", content: args?.[0] }],
}).then(res => {
console.log(res.messages[res.messages.length - 1].content);
}).catch(err => {
console.error(err);
});
}
在 .env 里配置好相关 LLM 服务的 key(本文用的 deepseek)
# .env
DEEPSEEK_API_KEY=在开放平台获取的 key
在根目录用命令行验证一下
npm start '概述下 0_deepagents 项目'
成功执行之后,可以获取类似输出:
于我对项目的分析,我来为您概述 **0_deepagents** 项目:
## 项目概述
**0_deepagents** 是一个基于 DeepSeek AI 和 DeepAgents 框架构建的代码助手代理项目,它是 `mini-coding-agent` 项目的一部分。
## 核心功能
1. **AI 驱动的代码助手**:使用 DeepSeek 的 Chat API 作为底层模型
2. **ReAct 架构**:采用 Reasoning and Acting 模式,能够解释用户指令并选择合适的工具执行
3. **文件系统集成**:通过 FilesystemBackend 与本地文件系统交互
4. **命令行接口**:支持通过命令行参数传递指令
## 技术栈
- **运行时环境**:Node.js (ES Module)
- **AI 模型**:DeepSeek Chat (通过 @langchain/deepseek 集成)
- **代理框架**:DeepAgents v1.7.0
- **配置管理**:dotenv 用于环境变量管理
- **追踪监控**:LangSmith 用于 API 调用追踪
## 项目结构
```
0_deepagents/
└── src/
└── index.js # 主入口文件,包含代理配置和启动逻辑
```
## 核心组件
### 1. **代理配置** (`codingAgent`)
- 使用 DeepSeek Chat 模型 (temperature=0, maxTokens=4096)
- 虚拟文件系统模式,工作目录为项目根目录
- 简洁的 ReAct 风格系统提示词
### 2. **系统提示词**
- 告知代理当前工作目录 (`PROJECT_ROOT`)
- 定义代理角色:作为 ReAct 编码代理,解释用户指令并使用最合适的工具执行
### 3. **命令行接口**
- 支持通过 `npm start` 或直接运行 `node ./0_deepagents/src/index.js` 启动
- 通过命令行参数传递用户指令
- 错误处理和用户友好的提示信息
## 使用方式
1. **环境配置**:需要设置 `DEEPSEEK_API_KEY` 环境变量
2. **启动代理**:
```bash
npm start "你的指令"
# 或
node ./0_deepagents/src/index.js "你的指令"
```
3. **功能示例**:
- 代码生成和修改
- 文件系统操作
- 项目分析和重构
## 项目特点
1. **轻量级设计**:代码简洁,专注于核心功能
2. **模块化架构**:易于扩展和集成其他工具
3. **生产就绪**:包含错误处理和日志追踪
4. **可配置性**:通过环境变量灵活配置 API 密钥和追踪设置
## 在整体项目中的位置
`0_deepagents` 是 `mini-coding-agent` 项目的第一个模块,后续还有 `1_langgraph` 等其他模块,共同构成一个完整的 AI 编码助手系统。
这个项目展示了如何将现代 AI 模型与代理框架结合,创建一个实用的代码助手工具,能够理解自然语言指令并执行相应的编码任务。
🎉🎉🎉 恭喜你!亲手实现了 Mini Coding Agent,下一步就是年薪百万了,下课!
1.2. 拆解魔法
你可能会一头雾水,这就实现了?我是谁?我在哪?怎么它就能正确的分析当前目录了?
稍安勿躁,我们慢慢开始解析
1.2.1. Agent 定义
首先从定义开始,到底什么是 Agent?
网上关于 Agent 的文章千千万,定义也不尽相同。我个人是这么理解的:
AI Agent = 具有特定范式,以及工具调用能力的 LLM
工具调用不必多说,范式简单来说就是实现目标的「思考方式」,比如是最简单的只执行一步,还是列完总体步骤分布执行,或者是执行一步看一步...
常见范式:
| Agent 范式类型 | 核心逻辑 |
|---|---|
| ReAct Agent | 思考-行动交替,无提前规划 |
| Plan&Execute Agent | 先规划、再按步骤执行 |
| Self-Ask Agent | 自我提问-工具验证,无修正 |
| ... | ... |
1.2.2. mca 的范式和工具
大部分 Coding Agent 的本质是一个 ReAct 风格的 Agent,mca 当然也一样
ReAct 是 Reasoning and Act 的缩写,是一种让 LLM 能够「边思考边行动」的提示范式,核心思想是让模型交替进行 Reasoning(推理)和 Acting(行动)。
基本流程:
- Thought(思考) :模型分析当前状态,决定下一步做什么
- Action(行动) :执行具体操作(比如调用搜索 API、读文件)
- Observation(观察) :获取行动结果
- 重复 1-3,直到得出最终答案
![]()
它拥有自己的规划、感知和执行能力:
- 通过调用只读的文件系统工具集(ls、read_file、grep...) 获得仓库的结构和必要的上下文,从而定位到与用户需求相关的文件和行号
- 然后再通过文本编辑器工具集(write_file、edit_file...) 将代码写入仓库
![]()
1.2.3. 验证内部逻辑
说了一大堆可能还是没什么实感,我们可以通过查看 mca 内部执行日志来更直观的感受
查日志的流程非常简单,在 deepagents 提供的日志服务 LangSmith 注册并获取 API key
![]()
在之前的 .env 中增加相关配置(无需修改任何代码)
# .env
DEEPSEEK_API_KEY=在开放平台获取的 key
+ LANGSMITH_TRACING=true
+ LANGSMITH_API_KEY=刚获取的 API key
+ LANGSMITH_PROJECT="mca"
重新执行刚才的 node 命令,就可以实时在 LangSmith 上查看相关日志了
![]()
这个界面非常直观,每一步做了什么、入参、出参都非常清晰。通过阅读日志,我们可以发现 mca 本质上就是基于 LLM 调用不同工具获取所需信息,信息足够之后就可以输出最终结果
比如点击到任意 Chat 节点,可以查看完整的 SystemPrompt、可用 Tools
![]()
除开最顶部两行是我们自定义的以外,下面都是 deepagents 自行添加的,主要功能是:
- 声明要记录 TODO(为了减少复杂度我们后续可以先忽略这一部分)
- 可用的文件系统相关工具
- 如何开启 Subagent(为了减少复杂度我们后续可以先忽略这一部分)
---
PROJECT_ROOT: /Users/bytedance/my-project/mini-cluade-code-study
---
// As a ReAct coding agent, interpret user instructions and execute them using the most suitable tool.
In order to complete the objective that the user asks of you, you have access to a number of standard tools.
## `write_todos`
You have access to the `write_todos` tool to help you manage and plan complex objectives.
Use this tool for complex objectives to ensure that you are tracking each necessary step and giving the user visibility into your progress.
This tool is very helpful for planning complex objectives, and for breaking down these larger complex objectives into smaller steps.
It is critical that you mark todos as completed as soon as you are done with a step. Do not batch up multiple steps before marking them as completed.
For simple objectives that only require a few steps, it is better to just complete the objective directly and NOT use this tool.
Writing todos takes time and tokens, use it when it is helpful for managing complex many-step problems! But not for simple few-step requests.
## Important To-Do List Usage Notes to Remember
- The `write_todos` tool should never be called multiple times in parallel.
- Don't be afraid to revise the To-Do list as you go. New information may reveal new tasks that need to be done, or old tasks that are irrelevant.
## Filesystem Tools `ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`
You have access to a filesystem which you can interact with using these tools.
All file paths must start with a /.
- ls: list files in a directory (requires absolute path)
- read_file: read a file from the filesystem
- write_file: write to a file in the filesystem
- edit_file: edit a file in the filesystem
- glob: find files matching a pattern (e.g., "**/*.py")
- grep: search for text within files
## `task` (subagent spawner)
You have access to a `task` tool to launch short-lived subagents that handle isolated tasks. These agents are ephemeral — they live only for the duration of the task and return a single result.
When to use the task tool:
- When a task is complex and multi-step, and can be fully delegated in isolation
- When a task is independent of other tasks and can run in parallel
- When a task requires focused reasoning or heavy token/context usage that would bloat the orchestrator thread
- When sandboxing improves reliability (e.g. code execution, structured searches, data formatting)
- When you only care about the output of the subagent, and not the intermediate steps (ex. performing a lot of research and then returned a synthesized report, performing a series of computations or lookups to achieve a concise, relevant answer.)
Subagent lifecycle:
1. **Spawn** → Provide clear role, instructions, and expected output
2. **Run** → The subagent completes the task autonomously
3. **Return** → The subagent provides a single structured result
4. **Reconcile** → Incorporate or synthesize the result into the main thread
When NOT to use the task tool:
- If you need to see the intermediate reasoning or steps after the subagent has completed (the task tool hides them)
- If the task is trivial (a few tool calls or simple lookup)
- If delegating does not reduce token usage, complexity, or context switching
- If splitting would add latency without benefit
## Important Task Tool Usage Notes to Remember
- Whenever possible, parallelize the work that you do. This is true for both tool_calls, and for tasks. Whenever you have independent steps to complete - make tool_calls, or kick off tasks (subagents) in parallel to accomplish them faster. This saves time for the user, which is incredibly important.
- Remember to use the `task` tool to silo independent tasks within a multi-part objective.
- You should use the `task` tool whenever you have a complex task that will take multiple steps, and is independent from other tasks that the agent needs to complete. These agents are highly competent and efficient.
到这里,我们已经基本清楚了要实现一个 mca 的全部条件!
- 提供文件读写工具
- LLM + Prompt + ReAct 范式
2. 10 分钟实现 Mini Coding Agent
接下来抛开 DeepAgents,尝试用 LangGraph 实现上减少了一层封装的 mca
LangGraph 是一个低层级的编排框架和运行时,用于构建、管理和部署长期运行、有状态的智能体。(可以理解为一堆常用 LLM 相关逻辑的语法糖 SDK )
为了更好理解不同的 Coding Agent,我们这次改为参照 Cladue Code 而不是 deepagents
2.1. 文件读写工具
具体要哪些工具、每个工具的功能是什么不同 Coding Agent 有自己的理解和实现。
- 本案例参考 Cladue Agent 相关工具
- 也可以直接参考第一个案例中的 deepagents
要分析代码简单来说就是读代码、写代码,所以我们主要实现读写相关的工具
| 工具 | 说明 |
|---|---|
| Bash | 在持久 shell 会话中执行 bash 命令,支持可选的超时和后台执行。 |
| BashOutput | 从正在运行或已完成的后台 bash shell 中获取输出。 |
| Edit | 在文件中执行精确的字符串替换。 |
| Read | 从本地文件系统读取文件,包括文本、图片、PDF 和 Jupyter notebook。 |
| Write | 将文件写入本地文件系统,如果文件已存在则覆盖。 |
| Glob | 快速文件模式匹配,适用于任何规模的代码库。 |
| Grep | 基于 ripgrep 构建的强大搜索工具,支持正则表达式。 |
本文重点是理解 Coding Agent,所以不会古法手搓这些 Tools,而是直接基于现有文档让 AI 辅助生成
2.1.1. LangChain 中关于如何创建一个工具的相关文档
![]()
这里只是给出复制之后的文档便于后续使用,不用细看。后面的长 Markdown 也类似
> ## Documentation Index
> Fetch the complete documentation index at: https://docs.langchain.com/llms.txt
> Use this file to discover all available pages before exploring further.
# Tools
Tools extend what [agents](/oss/javascript/langchain/agents) can do—letting them fetch real-time data, execute code, query external databases, and take actions in the world.
Under the hood, tools are callable functions with well-defined inputs and outputs that get passed to a [chat model](/oss/javascript/langchain/models). The model decides when to invoke a tool based on the conversation context, and what input arguments to provide.
<Tip>
For details on how models handle tool calls, see [Tool calling](/oss/javascript/langchain/models#tool-calling).
</Tip>
## Create tools
### Basic tool definition
The simplest way to create a tool is by importing the `tool` function from the `langchain` package. You can use [zod](https://zod.dev/) to define the tool's input schema:
```ts theme={null}
import * as z from "zod"
import { tool } from "langchain"
const searchDatabase = tool(
({ query, limit }) => `Found ${limit} results for '${query}'`,
{
name: "search_database",
description: "Search the customer database for records matching the query.",
schema: z.object({
query: z.string().describe("Search terms to look for"),
limit: z.number().describe("Maximum number of results to return"),
}),
}
);
```
<Note>
**Server-side tool use:** Some chat models feature built-in tools (web search, code interpreters) that are executed server-side. See [Server-side tool use](#server-side-tool-use) for details.
</Note>
<Warning>
Prefer `snake_case` for tool names (e.g., `web_search` instead of `Web Search`). Some model providers have issues with or reject names containing spaces or special characters with errors. Sticking to alphanumeric characters, underscores, and hyphens helps to improve compatibility across providers.
</Warning>
## Access context
Tools are most powerful when they can access runtime information like conversation history, user data, and persistent memory. This section covers how to access and update this information from within your tools.
### Context
Context provides immutable configuration data that is passed at invocation time. Use it for user IDs, session details, or application-specific settings that shouldn't change during a conversation.
Tools can access an agent's runtime context through the `config` parameter:
```ts theme={null}
import * as z from "zod"
import { ChatOpenAI } from "@langchain/openai"
import { createAgent } from "langchain"
const getUserName = tool(
(_, config) => {
return config.context.user_name
},
{
name: "get_user_name",
description: "Get the user's name.",
schema: z.object({}),
}
);
const contextSchema = z.object({
user_name: z.string(),
});
const agent = createAgent({
model: new ChatOpenAI({ model: "gpt-4.1" }),
tools: [getUserName],
contextSchema,
});
const result = await agent.invoke(
{
messages: [{ role: "user", content: "What is my name?" }]
},
{
context: { user_name: "John Smith" }
}
);
```
### Long-term memory (Store)
The [`BaseStore`](https://reference.langchain.com/javascript/langchain-core/stores/BaseStore) provides persistent storage that survives across conversations. Unlike state (short-term memory), data saved to the store remains available in future sessions.
Access the store through `config.store`. The store uses a namespace/key pattern to organize data:
```ts expandable theme={null}
import * as z from "zod";
import { createAgent, tool } from "langchain";
import { InMemoryStore } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
const store = new InMemoryStore();
// Access memory
const getUserInfo = tool(
async ({ user_id }) => {
const value = await store.get(["users"], user_id);
console.log("get_user_info", user_id, value);
return value;
},
{
name: "get_user_info",
description: "Look up user info.",
schema: z.object({
user_id: z.string(),
}),
}
);
// Update memory
const saveUserInfo = tool(
async ({ user_id, name, age, email }) => {
console.log("save_user_info", user_id, name, age, email);
await store.put(["users"], user_id, { name, age, email });
return "Successfully saved user info.";
},
{
name: "save_user_info",
description: "Save user info.",
schema: z.object({
user_id: z.string(),
name: z.string(),
age: z.number(),
email: z.string(),
}),
}
);
const agent = createAgent({
model: new ChatOpenAI({ model: "gpt-4.1" }),
tools: [getUserInfo, saveUserInfo],
store,
});
// First session: save user info
await agent.invoke({
messages: [
{
role: "user",
content: "Save the following user: userid: abc123, name: Foo, age: 25, email: foo@langchain.dev",
},
],
});
// Second session: get user info
const result = await agent.invoke({
messages: [
{ role: "user", content: "Get user info for user with id 'abc123'" },
],
});
console.log(result);
// Here is the user info for user with ID "abc123":
// - Name: Foo
// - Age: 25
// - Email: foo@langchain.dev
```
### Stream writer
Stream real-time updates from tools during execution. This is useful for providing progress feedback to users during long-running operations.
Use `config.writer` to emit custom updates:
```ts theme={null}
import * as z from "zod";
import { tool, ToolRuntime } from "langchain";
const getWeather = tool(
({ city }, config: ToolRuntime) => {
const writer = config.writer;
// Stream custom updates as the tool executes
if (writer) {
writer(`Looking up data for city: ${city}`);
writer(`Acquired data for city: ${city}`);
}
return `It's always sunny in ${city}!`;
},
{
name: "get_weather",
description: "Get weather for a given city.",
schema: z.object({
city: z.string(),
}),
}
);
```
## ToolNode
[`ToolNode`](https://reference.langchain.com/javascript/langchain-langgraph/prebuilt/ToolNode) is a prebuilt node that executes tools in LangGraph workflows. It handles parallel tool execution, error handling, and state injection automatically.
<Info>
For custom workflows where you need fine-grained control over tool execution patterns, use [`ToolNode`](https://reference.langchain.com/javascript/langchain-langgraph/prebuilt/ToolNode) instead of @[`create_agent`]. It's the building block that powers agent tool execution.
</Info>
### Basic usage
```typescript theme={null}
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { tool } from "@langchain/core/tools";
import * as z from "zod";
const search = tool(
({ query }) => `Results for: ${query}`,
{
name: "search",
description: "Search for information.",
schema: z.object({ query: z.string() }),
}
);
const calculator = tool(
({ expression }) => String(eval(expression)),
{
name: "calculator",
description: "Evaluate a math expression.",
schema: z.object({ expression: z.string() }),
}
);
// Create the ToolNode with your tools
const toolNode = new ToolNode([search, calculator]);
```
### Error handling
Configure how tool errors are handled. See the [`ToolNode`](https://reference.langchain.com/javascript/langchain-langgraph/prebuilt/ToolNode) API reference for all options.
```typescript theme={null}
import { ToolNode } from "@langchain/langgraph/prebuilt";
// Default behavior
const toolNode = new ToolNode(tools);
// Catch all errors
const toolNode = new ToolNode(tools, { handleToolErrors: true });
// Custom error message
const toolNode = new ToolNode(tools, {
handleToolErrors: "Something went wrong, please try again."
});
```
### Route with tools_condition
Use @[`tools_condition`] for conditional routing based on whether the LLM made tool calls:
```typescript theme={null}
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { StateGraph, MessagesAnnotation } from "@langchain/langgraph";
const builder = new StateGraph(MessagesAnnotation)
.addNode("llm", callLlm)
.addNode("tools", new ToolNode(tools))
.addEdge("__start__", "llm")
.addConditionalEdges("llm", toolsCondition) // Routes to "tools" or "__end__"
.addEdge("tools", "llm");
const graph = builder.compile();
```
### State injection
Tools can access the current graph state through @[`ToolRuntime`]:
For more details on accessing state, context, and long-term memory from tools, see [Access context](#access-context).
## Prebuilt tools
LangChain provides a large collection of prebuilt tools and toolkits for common tasks like web search, code interpretation, database access, and more. These ready-to-use tools can be directly integrated into your agents without writing custom code.
See the [tools and toolkits](/oss/javascript/integrations/tools) integration page for a complete list of available tools organized by category.
## Server-side tool use
Some chat models feature built-in tools that are executed server-side by the model provider. These include capabilities like web search and code interpreters that don't require you to define or host the tool logic.
Refer to the individual [chat model integration pages](/oss/javascript/integrations/providers) and the [tool calling documentation](/oss/javascript/langchain/models#server-side-tool-use) for details on enabling and using these built-in tools.
***
<Callout icon="edit">
[Edit this page on GitHub](https://github.com/langchain-ai/docs/edit/main/src/oss/langchain/tools.mdx) or [file an issue](https://github.com/langchain-ai/docs/issues/new/choose).
</Callout>
<Callout icon="terminal-2">
[Connect these docs](/use-these-docs) to Claude, VSCode, and more via MCP for real-time answers.
</Callout>
2.1.2. Claude Prompt 中关于各个工具的说明(其他人逆向获取的)
这里面包含了各个工具的作用、入参出参甚至调用时机等
## Bash
Executes a given bash command and returns its output.
The working directory persists between commands, but shell state does not. The shell environment is initialized from the user's profile (bash or zsh).
IMPORTANT: Avoid using this tool to run `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo` commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool as this will provide a much better experience for the user:
- File search: Use Glob (NOT find or ls)
- Content search: Use Grep (NOT grep or rg)
- Read files: Use Read (NOT cat/head/tail)
- Edit files: Use Edit (NOT sed/awk)
- Write files: Use Write (NOT echo >/cat <<EOF)
- Communication: Output text directly (NOT echo/printf)
While the Bash tool can do similar things, it’s better to use the built-in tools as they provide a better user experience and make it easier to review tool calls and give permission.
### Instructions
- If your command will create new directories or files, first use this tool to run `ls` to verify the parent directory exists and is the correct location.
- Always quote file paths that contain spaces with double quotes in your command (e.g., cd "path with spaces/file.txt")
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
- You may specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). By default, your command will timeout after 120000ms (2 minutes).
- - You can use the `run_in_background` parameter to run the command in the background. Only use this if you don't need the result immediately and are OK being notified when the command completes later. You do not need to check the output right away - you'll be notified when it finishes. You do not need to use '&' at the end of the command when using this parameter.
- Write a clear, concise description of what your command does. For simple commands, keep it brief (5-10 words). For complex commands (piped commands, obscure flags, or anything hard to understand at a glance), include enough context so that the user can understand what your command will do.
- When issuing multiple commands:
- If the commands are independent and can run in parallel, make multiple Bash tool calls in a single message. Example: if you need to run "git status" and "git diff", send a single message with two Bash tool calls in parallel.
- If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together.
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail.
- DO NOT use newlines to separate commands (newlines are ok in quoted strings).
- For git commands:
- Prefer to create a new commit rather than amending an existing commit.
- Before running destructive operations (e.g., git reset --hard, git push --force, git checkout --), consider whether there is a safer alternative that achieves the same goal. Only use destructive operations when they are truly the best approach.
- Never skip hooks (--no-verify) or bypass signing (--no-gpg-sign, -c commit.gpgsign=false) unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue.
- Avoid unnecessary `sleep` commands:
- Do not sleep between commands that can run immediately — just run them.
- If your command is long running and you would like to be notified when it finishes – simply run your command using `run_in_background`. There is no need to sleep in this case.
- Do not retry failing commands in a sleep loop — diagnose the root cause or consider an alternative approach.
- If waiting for a background task you started with `run_in_background`, you will be notified when it completes — do not poll.
- If you must poll an external process, use a check command (e.g. `gh run view`) rather than sleeping first.
- If you must sleep, keep the duration short (1-5 seconds) to avoid blocking the user.
### Committing changes with git
Only create commits when requested by the user. If unclear, ask first. When the user asks you to create a new git commit, follow these steps carefully:
Git Safety Protocol:
- NEVER update the git config
- NEVER run destructive git commands (push --force, reset --hard, checkout ., restore ., clean -f, branch -D) unless the user explicitly requests these actions. Taking unauthorized destructive actions is unhelpful and can result in lost work, so it's best to ONLY run these commands when given direct instructions
- NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it
- NEVER run force push to main/master, warn the user if they request it
- CRITICAL: Always create NEW commits rather than amending, unless the user explicitly requests a git amend. When a pre-commit hook fails, the commit did NOT happen — so --amend would modify the PREVIOUS commit, which may result in destroying work or losing previous changes. Instead, after hook failure, fix the issue, re-stage, and create a NEW commit
- When staging files, prefer adding specific files by name rather than using "git add -A" or "git add .", which can accidentally include sensitive files (.env, credentials) or large binaries
- NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive
1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following bash commands in parallel, each using the Bash tool:
- Run a git status command to see all untracked files. IMPORTANT: Never use the -uall flag as it can cause memory issues on large repos.
- Run a git diff command to see both staged and unstaged changes that will be committed.
- Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.
2. Analyze all staged changes (both previously staged and newly added) and draft a commit message:
- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.). Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.).
- Do not commit files that likely contain secrets (.env, credentials.json, etc). Warn the user if they specifically request to commit those files
- Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what"
- Ensure it accurately reflects the changes and their purpose
3. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following commands:
- Add relevant untracked files to the staging area.
- Create the commit with a message ending with:
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Run git status after the commit completes to verify success.
Note: git status depends on the commit completing, so run it sequentially after the commit.
4. If the commit fails due to pre-commit hook: fix the issue and create a NEW commit
Important notes:
- NEVER run additional commands to read or explore code, besides git bash commands
- NEVER use the TodoWrite or Task tools
- DO NOT push to the remote repository unless the user explicitly asks you to do so
- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.
- IMPORTANT: Do not use --no-edit with git rebase commands, as the --no-edit flag is not a valid option for git rebase.
- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit
- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
<example>
git commit -m "$(cat <<'EOF'
Commit message here.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EOF
)"
</example>
### Creating pull requests
Use the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.
IMPORTANT: When the user asks you to create a pull request, follow these steps carefully:
1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following bash commands in parallel using the Bash tool, in order to understand the current state of the branch since it diverged from the main branch:
- Run a git status command to see all untracked files (never use -uall flag)
- Run a git diff command to see both staged and unstaged changes that will be committed
- Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote
- Run a git log command and `git diff [base-branch]...HEAD` to understand the full commit history for the current branch (from the time it diverged from the base branch)
2. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (NOT just the latest commit, but ALL commits that will be included in the pull request!!!), and draft a pull request title and summary:
- Keep the PR title short (under 70 characters)
- Use the description/body for details, not the title
3. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following commands in parallel:
- Create new branch if needed
- Push to remote with -u flag if needed
- Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.
<example>
gh pr create --title "the pr title" --body "$(cat <<'EOF'
#### Summary
<1-3 bullet points>
#### Test plan
[Bulleted markdown checklist of TODOs for testing the pull request...]
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
</example>
Important:
- DO NOT use the TodoWrite or Task tools
- Return the PR URL when you're done, so the user can see it
### Other common operations
- View comments on a Github PR: gh api repos/foo/bar/pulls/123/comments
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"command": {
"description": "The command to execute",
"type": "string"
},
"timeout": {
"description": "Optional timeout in milliseconds (max 600000)",
"type": "number"
},
"description": {
"description": "Clear, concise description of what this command does in active voice. Never use words like "complex" or "risk" in the description - just describe what it does.\n\nFor simple commands (git, npm, standard CLI tools), keep it brief (5-10 words):\n- ls → "List files in current directory"\n- git status → "Show working tree status"\n- npm install → "Install package dependencies"\n\nFor commands that are harder to parse at a glance (piped commands, obscure flags, etc.), add enough context to clarify what it does:\n- find . -name "*.tmp" -exec rm {} \; → "Find and delete all .tmp files recursively"\n- git reset --hard origin/main → "Discard all local changes and match remote main"\n- curl -s url | jq '.data[]' → "Fetch JSON from URL and extract data array elements"",
"type": "string"
},
"run_in_background": {
"description": "Set to true to run this command in the background. Use TaskOutput to read the output later.",
"type": "boolean"
},
"dangerouslyDisableSandbox": {
"description": "Set this to true to dangerously override sandbox mode and run commands without sandboxing.",
"type": "boolean"
}
},
"required": [
"command"
],
"additionalProperties": false
}
---
## Edit
Performs exact string replacements in files.
Usage:
- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
- The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.
- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"file_path": {
"description": "The absolute path to the file to modify",
"type": "string"
},
"old_string": {
"description": "The text to replace",
"type": "string"
},
"new_string": {
"description": "The text to replace it with (must be different from old_string)",
"type": "string"
},
"replace_all": {
"description": "Replace all occurrences of old_string (default false)",
"default": false,
"type": "boolean"
}
},
"required": [
"file_path",
"old_string",
"new_string"
],
"additionalProperties": false
}
---
## Glob
- Fast file pattern matching tool that works with any codebase size
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
- Returns matching file paths sorted by modification time
- Use this tool when you need to find files by name patterns
- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead
- You can call multiple tools in a single response. It is always better to speculatively perform multiple searches in parallel if they are potentially useful.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"pattern": {
"description": "The glob pattern to match files against",
"type": "string"
},
"path": {
"description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.",
"type": "string"
}
},
"required": [
"pattern"
],
"additionalProperties": false
}
---
## Grep
A powerful search tool built on ripgrep
Usage:
- ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The Grep tool has been optimized for correct permissions and access.
- Supports full regex syntax (e.g., "log.*Error", "function\s+\w+")
- Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
- Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
- Use Task tool for open-ended searches requiring multiple rounds
- Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use `interface{}` to find `interface{}` in Go code)
- Multiline matching: By default patterns match within single lines only. For cross-line patterns like `struct {[\s\S]*?field`, use `multiline: true`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"pattern": {
"description": "The regular expression pattern to search for in file contents",
"type": "string"
},
"path": {
"description": "File or directory to search in (rg PATH). Defaults to current working directory.",
"type": "string"
},
"glob": {
"description": "Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}") - maps to rg --glob",
"type": "string"
},
"output_mode": {
"description": "Output mode: "content" shows matching lines (supports -A/-B/-C context, -n line numbers, head_limit), "files_with_matches" shows file paths (supports head_limit), "count" shows match counts (supports head_limit). Defaults to "files_with_matches".",
"type": "string",
"enum": [
"content",
"files_with_matches",
"count"
]
},
"-B": {
"description": "Number of lines to show before each match (rg -B). Requires output_mode: "content", ignored otherwise.",
"type": "number"
},
"-A": {
"description": "Number of lines to show after each match (rg -A). Requires output_mode: "content", ignored otherwise.",
"type": "number"
},
"-C": {
"description": "Alias for context.",
"type": "number"
},
"context": {
"description": "Number of lines to show before and after each match (rg -C). Requires output_mode: "content", ignored otherwise.",
"type": "number"
},
"-n": {
"description": "Show line numbers in output (rg -n). Requires output_mode: "content", ignored otherwise. Defaults to true.",
"type": "boolean"
},
"-i": {
"description": "Case insensitive search (rg -i)",
"type": "boolean"
},
"type": {
"description": "File type to search (rg --type). Common types: js, py, rust, go, java, etc. More efficient than include for standard file types.",
"type": "string"
},
"head_limit": {
"description": "Limit output to first N lines/entries, equivalent to "| head -N". Works across all output modes: content (limits output lines), files_with_matches (limits file paths), count (limits count entries). Defaults to 0 (unlimited).",
"type": "number"
},
"offset": {
"description": "Skip first N lines/entries before applying head_limit, equivalent to "| tail -n +N | head -N". Works across all output modes. Defaults to 0.",
"type": "number"
},
"multiline": {
"description": "Enable multiline mode where . matches newlines and patterns can span lines (rg -U --multiline-dotall). Default: false.",
"type": "boolean"
}
},
"required": [
"pattern"
],
"additionalProperties": false
}
---
## Read
Reads a file from the local filesystem. You can access any file directly by using this tool.
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
Usage:
- The file_path parameter must be an absolute path, not a relative path
- By default, it reads up to 2000 lines starting from the beginning of the file
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
- Any lines longer than 2000 characters will be truncated
- Results are returned using cat -n format, with line numbers starting at 1
- This tool allows Claude Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.
- This tool can read PDF files (.pdf). For large PDFs (more than 10 pages), you MUST provide the pages parameter to read specific page ranges (e.g., pages: "1-5"). Reading a large PDF without the pages parameter will fail. Maximum 20 pages per request.
- This tool can read Jupyter notebooks (.ipynb files) and returns all cells with their outputs, combining code, text, and visualizations.
- This tool can only read files, not directories. To read a directory, use an ls command via the Bash tool.
- You can call multiple tools in a single response. It is always better to speculatively read multiple potentially useful files in parallel.
- You will regularly be asked to read screenshots. If the user provides a path to a screenshot, ALWAYS use this tool to view the file at the path. This tool will work with all temporary file paths.
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"file_path": {
"description": "The absolute path to the file to read",
"type": "string"
},
"offset": {
"description": "The line number to start reading from. Only provide if the file is too large to read at once",
"type": "number"
},
"limit": {
"description": "The number of lines to read. Only provide if the file is too large to read at once.",
"type": "number"
},
"pages": {
"description": "Page range for PDF files (e.g., "1-5", "3", "10-20"). Only applicable to PDF files. Maximum 20 pages per request.",
"type": "string"
}
},
"required": [
"file_path"
],
"additionalProperties": false
}
---
## Write
Writes a file to the local filesystem.
Usage:
- This tool will overwrite the existing file if there is one at the provided path.
- If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.
- Prefer the Edit tool for modifying existing files — it only sends the diff. Only use this tool to create new files or for complete rewrites.
- NEVER create documentation files (*.md) or README files unless explicitly requested by the User.
- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"file_path": {
"description": "The absolute path to the file to write (must be absolute, not relative)",
"type": "string"
},
"content": {
"description": "The content to write to the file",
"type": "string"
}
},
"required": [
"file_path",
"content"
],
"additionalProperties": false
}
2.1.3. 聚合上述文档
创建 1_langgraph 项目,把上述文档放入 md 文件夹中,然后让任何 Coding Agent (Cladue Code、Trae、Cursor...)参考 md 中的相关文档,在 tools 生成代码
mca
├── .env
├── 0_deepagents
├── 1_langgraph
│ └── src
│ ├── md
│ │ ├── how_to_create_a_tool.md
│ │ └── a_list_of_all_required_tools.md
│ └── tools (空文件夹)
└── package.json
你也可以直接用上一步自己创建的 mca
npm start '基于 1_langgraph/src/md 中的 how_to_create_a_tool 在 1_langgraph/src/tools 中实现 a_list_of_all_required_tools 里全部工具'
2.1.4. 预期效果
顺利的话,你会得到创建好了所需 tools 的文件。如果生成内容有误,可以根据内容调整 input 让其重新生成或修改
mca
├── .env
├── 0_deepagents
├── 1_langgraph
│ └── src
│ ├── md
│ └── tools
+│ ├── bash.js
+│ ├── edit.js
+│ ├── glob.js
+│ ├── grep.js
+│ ├── index.js
+│ ├── read.js
+│ └── write.js
└── package.json
2.2. ReAct 范式
工具有了,接下来我们实现 ReAct 范式
2.2.1. 关于创建 LangGraph 应用的相关文档
和之前类似,我们在 md 文件中新增 lang_graph_quick_start.md,让 Coding Agent 知道要怎么创建一个 LangGraph 应用
![]()
> ## Documentation Index
> Fetch the complete documentation index at: https://docs.langchain.com/llms.txt
> Use this file to discover all available pages before exploring further.
# Quickstart
This quickstart demonstrates how to build a calculator agent using the LangGraph Graph API or the Functional API.
* [Use the Graph API](#use-the-graph-api) if you prefer to define your agent as a graph of nodes and edges.
* [Use the Functional API](#use-the-functional-api) if you prefer to define your agent as a single function.
For conceptual information, see [Graph API overview](/oss/javascript/langgraph/graph-api) and [Functional API overview](/oss/javascript/langgraph/functional-api).
<Info>
For this example, you will need to set up a [Claude (Anthropic)](https://www.anthropic.com/) account and get an API key. Then, set the `ANTHROPIC_API_KEY` environment variable in your terminal.
</Info>
<Tabs>
<Tab title="Use the Graph API">
## 1. Define tools and model
In this example, we'll use the Claude Sonnet 4.5 model and define tools for addition, multiplication, and division.
```typescript theme={null}
import { ChatAnthropic } from "@langchain/anthropic";
import { tool } from "@langchain/core/tools";
import * as z from "zod";
const model = new ChatAnthropic({
model: "claude-sonnet-4-5-20250929",
temperature: 0,
});
// Define tools
const add = tool(({ a, b }) => a + b, {
name: "add",
description: "Add two numbers",
schema: z.object({
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
}),
});
const multiply = tool(({ a, b }) => a * b, {
name: "multiply",
description: "Multiply two numbers",
schema: z.object({
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
}),
});
const divide = tool(({ a, b }) => a / b, {
name: "divide",
description: "Divide two numbers",
schema: z.object({
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
}),
});
// Augment the LLM with tools
const toolsByName = {
[add.name]: add,
[multiply.name]: multiply,
[divide.name]: divide,
};
const tools = Object.values(toolsByName);
const modelWithTools = model.bindTools(tools);
```
## 2. Define state
The graph's state is used to store the messages and the number of LLM calls.
<Tip>
State in LangGraph persists throughout the agent's execution.
The `MessagesValue` provides a built-in reducer for appending messages. The `llmCalls` field uses a `ReducedValue` with `(x, y) => x + y` to accumulate the count.
</Tip>
```typescript theme={null}
import {
StateGraph,
StateSchema,
MessagesValue,
ReducedValue,
GraphNode,
ConditionalEdgeRouter,
START,
END,
} from "@langchain/langgraph";
import { z } from "zod/v4";
const MessagesState = new StateSchema({
messages: MessagesValue,
llmCalls: new ReducedValue(
z.number().default(0),
{ reducer: (x, y) => x + y }
),
});
```
## 3. Define model node
The model node is used to call the LLM and decide whether to call a tool or not.
```typescript theme={null}
import { SystemMessage } from "@langchain/core/messages";
const llmCall: GraphNode<typeof MessagesState> = async (state) => {
const response = await modelWithTools.invoke([
new SystemMessage(
"You are a helpful assistant tasked with performing arithmetic on a set of inputs."
),
...state.messages,
]);
return {
messages: [response],
llmCalls: 1,
};
};
```
## 4. Define tool node
The tool node is used to call the tools and return the results.
```typescript theme={null}
import { AIMessage, ToolMessage } from "@langchain/core/messages";
const toolNode: GraphNode<typeof MessagesState> = async (state) => {
const lastMessage = state.messages.at(-1);
if (lastMessage == null || !AIMessage.isInstance(lastMessage)) {
return { messages: [] };
}
const result: ToolMessage[] = [];
for (const toolCall of lastMessage.tool_calls ?? []) {
const tool = toolsByName[toolCall.name];
const observation = await tool.invoke(toolCall);
result.push(observation);
}
return { messages: result };
};
```
## 5. Define end logic
The conditional edge function is used to route to the tool node or end based upon whether the LLM made a tool call.
```typescript theme={null}
const shouldContinue: ConditionalEdgeRouter<typeof MessagesState, "toolNode"> = (state) => {
const lastMessage = state.messages.at(-1);
// Check if it's an AIMessage before accessing tool_calls
if (!lastMessage || !AIMessage.isInstance(lastMessage)) {
return END;
}
// If the LLM makes a tool call, then perform an action
if (lastMessage.tool_calls?.length) {
return "toolNode";
}
// Otherwise, we stop (reply to the user)
return END;
};
```
## 6. Build and compile the agent
The agent is built using the [`StateGraph`](https://reference.langchain.com/javascript/langchain-langgraph/index/StateGraph) class and compiled using the [`compile`](https://reference.langchain.com/javascript/classes/_langchain_langgraph.index.StateGraph.html#compile) method.
```typescript theme={null}
const agent = new StateGraph(MessagesState)
.addNode("llmCall", llmCall)
.addNode("toolNode", toolNode)
.addEdge(START, "llmCall")
.addConditionalEdges("llmCall", shouldContinue, ["toolNode", END])
.addEdge("toolNode", "llmCall")
.compile();
// Invoke
import { HumanMessage } from "@langchain/core/messages";
const result = await agent.invoke({
messages: [new HumanMessage("Add 3 and 4.")],
});
for (const message of result.messages) {
console.log(`[${message.type}]: ${message.text}`);
}
```
<Tip>
To learn how to trace your agent with LangSmith, see the [LangSmith documentation](/langsmith/trace-with-langgraph).
</Tip>
Congratulations! You've built your first agent using the LangGraph Graph API.
<Accordion title="Full code example">
```typescript theme={null}
// Step 1: Define tools and model
import { ChatAnthropic } from "@langchain/anthropic";
import { tool } from "@langchain/core/tools";
import * as z from "zod";
const model = new ChatAnthropic({
model: "claude-sonnet-4-5-20250929",
temperature: 0,
});
// Define tools
const add = tool(({ a, b }) => a + b, {
name: "add",
description: "Add two numbers",
schema: z.object({
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
}),
});
const multiply = tool(({ a, b }) => a * b, {
name: "multiply",
description: "Multiply two numbers",
schema: z.object({
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
}),
});
const divide = tool(({ a, b }) => a / b, {
name: "divide",
description: "Divide two numbers",
schema: z.object({
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
}),
});
// Augment the LLM with tools
const toolsByName = {
[add.name]: add,
[multiply.name]: multiply,
[divide.name]: divide,
};
const tools = Object.values(toolsByName);
const modelWithTools = model.bindTools(tools);
```
```typescript theme={null}
// Step 2: Define state
import {
StateGraph,
StateSchema,
MessagesValue,
ReducedValue,
GraphNode,
ConditionalEdgeRouter,
START,
END,
} from "@langchain/langgraph";
import * as z from "zod";
const MessagesState = new StateSchema({
messages: MessagesValue,
llmCalls: new ReducedValue(
z.number().default(0),
{ reducer: (x, y) => x + y }
),
});
```
```typescript theme={null}
// Step 3: Define model node
import { SystemMessage, AIMessage, ToolMessage } from "@langchain/core/messages";
const llmCall: GraphNode<typeof MessagesState> = async (state) => {
return {
messages: [await modelWithTools.invoke([
new SystemMessage(
"You are a helpful assistant tasked with performing arithmetic on a set of inputs."
),
...state.messages,
])],
llmCalls: 1,
};
};
// Step 4: Define tool node
const toolNode: GraphNode<typeof MessagesState> = async (state) => {
const lastMessage = state.messages.at(-1);
if (lastMessage == null || !AIMessage.isInstance(lastMessage)) {
return { messages: [] };
}
const result: ToolMessage[] = [];
for (const toolCall of lastMessage.tool_calls ?? []) {
const tool = toolsByName[toolCall.name];
const observation = await tool.invoke(toolCall);
result.push(observation);
}
return { messages: result };
};
```
```typescript theme={null}
// Step 5: Define logic to determine whether to end
import { ConditionalEdgeRouter, END } from "@langchain/langgraph";
const shouldContinue: ConditionalEdgeRouter<typeof MessagesState, "toolNode"> = (state) => {
const lastMessage = state.messages.at(-1);
// Check if it's an AIMessage before accessing tool_calls
if (!lastMessage || !AIMessage.isInstance(lastMessage)) {
return END;
}
// If the LLM makes a tool call, then perform an action
if (lastMessage.tool_calls?.length) {
return "toolNode";
}
// Otherwise, we stop (reply to the user)
return END;
};
```
```typescript theme={null}
// Step 6: Build and compile the agent
import { HumanMessage } from "@langchain/core/messages";
import { StateGraph, START, END } from "@langchain/langgraph";
const agent = new StateGraph(MessagesState)
.addNode("llmCall", llmCall)
.addNode("toolNode", toolNode)
.addEdge(START, "llmCall")
.addConditionalEdges("llmCall", shouldContinue, ["toolNode", END])
.addEdge("toolNode", "llmCall")
.compile();
// Invoke
const result = await agent.invoke({
messages: [new HumanMessage("Add 3 and 4.")],
});
for (const message of result.messages) {
console.log(`[${message.type}]: ${message.text}`);
}
```
</Accordion>
</Tab>
<Tab title="Use the Functional API">
## 1. Define tools and model
In this example, we'll use the Claude Sonnet 4.5 model and define tools for addition, multiplication, and division.
```typescript theme={null}
import { ChatAnthropic } from "@langchain/anthropic";
import { tool } from "@langchain/core/tools";
import * as z from "zod";
const model = new ChatAnthropic({
model: "claude-sonnet-4-5-20250929",
temperature: 0,
});
// Define tools
const add = tool(({ a, b }) => a + b, {
name: "add",
description: "Add two numbers",
schema: z.object({
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
}),
});
const multiply = tool(({ a, b }) => a * b, {
name: "multiply",
description: "Multiply two numbers",
schema: z.object({
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
}),
});
const divide = tool(({ a, b }) => a / b, {
name: "divide",
description: "Divide two numbers",
schema: z.object({
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
}),
});
// Augment the LLM with tools
const toolsByName = {
[add.name]: add,
[multiply.name]: multiply,
[divide.name]: divide,
};
const tools = Object.values(toolsByName);
const modelWithTools = model.bindTools(tools);
```
## 2. Define model node
The model node is used to call the LLM and decide whether to call a tool or not.
```typescript theme={null}
import { task, entrypoint } from "@langchain/langgraph";
import { SystemMessage } from "@langchain/core/messages";
const callLlm = task({ name: "callLlm" }, async (messages: BaseMessage[]) => {
return modelWithTools.invoke([
new SystemMessage(
"You are a helpful assistant tasked with performing arithmetic on a set of inputs."
),
...messages,
]);
});
```
## 3. Define tool node
The tool node is used to call the tools and return the results.
```typescript theme={null}
import type { ToolCall } from "@langchain/core/messages/tool";
const callTool = task({ name: "callTool" }, async (toolCall: ToolCall) => {
const tool = toolsByName[toolCall.name];
return tool.invoke(toolCall);
});
```
## 4. Define agent
```typescript theme={null}
import { addMessages } from "@langchain/langgraph";
import { type BaseMessage } from "@langchain/core/messages";
const agent = entrypoint({ name: "agent" }, async (messages: BaseMessage[]) => {
let modelResponse = await callLlm(messages);
while (true) {
if (!modelResponse.tool_calls?.length) {
break;
}
// Execute tools
const toolResults = await Promise.all(
modelResponse.tool_calls.map((toolCall) => callTool(toolCall))
);
messages = addMessages(messages, [modelResponse, ...toolResults]);
modelResponse = await callLlm(messages);
}
return messages;
});
// Invoke
import { HumanMessage } from "@langchain/core/messages";
const result = await agent.invoke([new HumanMessage("Add 3 and 4.")]);
for (const message of result) {
console.log(`[${message.getType()}]: ${message.text}`);
}
```
<Tip>
To learn how to trace your agent with LangSmith, see the [LangSmith documentation](/langsmith/trace-with-langgraph).
</Tip>
Congratulations! You've built your first agent using the LangGraph Functional API.
<Accordion title="Full code example" icon="code">
```typescript theme={null}
import { ChatAnthropic } from "@langchain/anthropic";
import { tool } from "@langchain/core/tools";
import {
task,
entrypoint,
addMessages,
} from "@langchain/langgraph";
import {
SystemMessage,
HumanMessage,
type BaseMessage,
} from "@langchain/core/messages";
import type { ToolCall } from "@langchain/core/messages/tool";
import * as z from "zod";
// Step 1: Define tools and model
const model = new ChatAnthropic({
model: "claude-sonnet-4-5-20250929",
temperature: 0,
});
// Define tools
const add = tool(({ a, b }) => a + b, {
name: "add",
description: "Add two numbers",
schema: z.object({
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
}),
});
const multiply = tool(({ a, b }) => a * b, {
name: "multiply",
description: "Multiply two numbers",
schema: z.object({
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
}),
});
const divide = tool(({ a, b }) => a / b, {
name: "divide",
description: "Divide two numbers",
schema: z.object({
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
}),
});
// Augment the LLM with tools
const toolsByName = {
[add.name]: add,
[multiply.name]: multiply,
[divide.name]: divide,
};
const tools = Object.values(toolsByName);
const modelWithTools = model.bindTools(tools);
// Step 2: Define model node
const callLlm = task({ name: "callLlm" }, async (messages: BaseMessage[]) => {
return modelWithTools.invoke([
new SystemMessage(
"You are a helpful assistant tasked with performing arithmetic on a set of inputs."
),
...messages,
]);
});
// Step 3: Define tool node
const callTool = task({ name: "callTool" }, async (toolCall: ToolCall) => {
const tool = toolsByName[toolCall.name];
return tool.invoke(toolCall);
});
// Step 4: Define agent
const agent = entrypoint({ name: "agent" }, async (messages: BaseMessage[]) => {
let modelResponse = await callLlm(messages);
while (true) {
if (!modelResponse.tool_calls?.length) {
break;
}
// Execute tools
const toolResults = await Promise.all(
modelResponse.tool_calls.map((toolCall) => callTool(toolCall))
);
messages = addMessages(messages, [modelResponse, ...toolResults]);
modelResponse = await callLlm(messages);
}
return messages;
});
// Invoke
const result = await agent.invoke([new HumanMessage("Add 3 and 4.")]);
for (const message of result) {
console.log(`[${message.type}]: ${message.text}`);
}
```
</Accordion>
</Tab>
</Tabs>
***
<Callout icon="edit">
[Edit this page on GitHub](https://github.com/langchain-ai/docs/edit/main/src/oss/langgraph/quickstart.mdx) or [file an issue](https://github.com/langchain-ai/docs/issues/new/choose).
</Callout>
<Callout icon="terminal-2">
[Connect these docs](/use-these-docs) to Claude, VSCode, and more via MCP for real-time answers.
</Callout>
2.2.2. 逻辑生成
同样让 Coding Agent 辅助生成代码
npm start "基于 1_langgraph/src/md/lang_graph_quick_start.md,在 1_langgraph/src/index.js 生成一个 ReAct 风格的 Coding Agent,注册 1_langgraph/src/tools 中的全部工具"
最终结果基于你所用的 Agent、模型会有差异。这里贴出我基于 claude-opus-4-6 生成同时证过的代码
import "dotenv/config";
import { ChatDeepSeek } from "@langchain/deepseek";
import {
StateGraph,
MessagesAnnotation,
START,
END,
} from "@langchain/langgraph";
import {
SystemMessage,
HumanMessage,
AIMessage,
} from "@langchain/core/messages";
import allTools from "./tools/index.js";
// 项目根目录
const PROJECT_ROOT = process.cwd();
// ReAct Coding Agent 系统提示词
const SYSTEM_PROMPT = `---
PROJECT_ROOT: ${PROJECT_ROOT}
---
You are a ReAct-style coding agent. You have access to a set of tools for interacting with the filesystem and executing commands.
When given a task:
1. Think about what you need to do
2. Use the appropriate tool to gather information or make changes
3. Observe the result
4. Repeat until the task is complete
`;
// 构建 toolsByName 映射
const toolsByName = {};
for (const t of allTools) {
toolsByName[t.name] = t;
}
// 将工具绑定到模型
const model = new ChatDeepSeek({
model: "deepseek-chat",
temperature: 0,
maxTokens: 4096,
apiKey: process.env.DEEPSEEK_API_KEY,
});
const modelWithTools = model.bindTools(allTools);
// Step 1: 模型节点 — 调用 LLM 决定是否使用工具
const llmCall = async (state) => {
const response = await modelWithTools.invoke([
new SystemMessage(SYSTEM_PROMPT),
...state.messages,
]);
return { messages: [response] };
};
// Step 2: 工具节点 — 执行 LLM 请求的工具调用
const toolNode = async (state) => {
const lastMessage = state.messages.at(-1);
if (!lastMessage || !AIMessage.isInstance(lastMessage)) {
return { messages: [] };
}
const results = [];
for (const toolCall of lastMessage.tool_calls ?? []) {
const tool = toolsByName[toolCall.name];
if (!tool) {
results.push({
role: "tool",
content: `Error: Unknown tool "${toolCall.name}"`,
tool_call_id: toolCall.id,
});
continue;
}
const observation = await tool.invoke(toolCall);
results.push(observation);
}
return { messages: results };
};
// Step 3: 条件路由 — 有 tool_calls 则继续,否则结束
const shouldContinue = (state) => {
const lastMessage = state.messages.at(-1);
if (!lastMessage || !AIMessage.isInstance(lastMessage)) {
return END;
}
if (lastMessage.tool_calls?.length) {
return "toolNode";
}
return END;
};
// Step 4: 构建并编译 StateGraph
const agent = new StateGraph(MessagesAnnotation)
.addNode("llmCall", llmCall)
.addNode("toolNode", toolNode)
.addEdge(START, "llmCall")
.addConditionalEdges("llmCall", shouldContinue, ["toolNode", END])
.addEdge("toolNode", "llmCall")
.compile();
// CLI 入口
const args = process.argv.slice(2);
if (args.length === 0) {
console.log("请输入指令");
process.exit(1);
}
const result = await agent.invoke(
{ messages: [new HumanMessage(args[0])] },
{ recursionLimit: 100 },
);
// 输出最后一条消息
const lastMsg = result.messages[result.messages.length - 1];
console.log(lastMsg.content);
看着一大堆,其实 ReAct 的核心逻辑只在 108~114 行的 Graph(图)构造和 21~31 行的 Prompt 部分
| 代码 Graph 图示 | 先前绘制的 ReAct 图示 |
|---|---|
用人话来说就是在调用 LLM 的时候和模型说「如果现有对话已经包含全部所需信息,那么你就输出结果,否则返回希望调用什么工具、获取什么信息」
2.2.3. 验证结果
工具 + ReAct 范式代码、Prompt 都已经完成,那么 mca 就可以直接运行了(记得在 package.json 中定义一下 script)
npm run start:langgraph "分析 1_langgraph 项目"
现在我已经对项目有了全面的了解。让我总结一下分析结果:
## 1_langgraph 项目分析报告
### 项目概述
这是一个基于 LangGraph 框架构建的 ReAct 风格编码代理项目。项目使用 LangGraph 的 StateGraph API 来创建一个能够执行文件系统操作和命令执行的智能代理。
### 项目结构
```
1_langgraph/
├── src/
│ ├── index.js # 主入口文件,包含 LangGraph 代理实现
│ ├── tools/ # 工具模块
│ │ ├── index.js # 工具导出文件
│ │ ├── bash.js # bash 命令执行工具
│ │ ├── edit.js # 文件编辑工具
│ │ ├── glob.js # 文件模式匹配工具
│ │ ├── grep.js # 文件内容搜索工具
│ │ ├── read.js # 文件读取工具
│ │ └── write.js # 文件写入工具
│ └── md/ # 文档文件
│ ├── lang_graph_quick_start.md # LangGraph 快速入门指南
│ ├── how_to_create_a_tool.md # 工具创建指南
│ └── a_list_of_all_required_tools.md # 所需工具列表
```
### 核心技术栈
1. **LangGraph**: 用于构建有状态的代理工作流
2. **DeepSeek API**: 作为 LLM 模型提供者
3. **Zod**: 用于工具参数的模式验证
4. **Node.js**: 运行时环境
### 核心功能
#### 1. 代理架构
项目实现了经典的 ReAct (Reasoning + Acting) 模式:
- **LLM 节点**: 调用 DeepSeek 模型决定是否使用工具
- **工具节点**: 执行 LLM 请求的工具调用
- **条件路由**: 根据是否有工具调用决定继续执行还是结束
#### 2. 可用工具
项目提供了 6 个核心工具:
1. **bash**: 执行 shell 命令,支持工作目录持久化
2. **read**: 读取文件内容,支持行号显示和分页
3. **write**: 写入文件,用于创建新文件或完全重写
4. **edit**: 精确字符串替换编辑文件
5. **glob**: 文件模式匹配搜索
6. **grep**: 正则表达式文件内容搜索
#### 3. 状态管理
使用 LangGraph 的 `MessagesAnnotation` 来管理消息状态,支持多轮对话和工具调用。
### 运行方式
```bash
# 通过 npm 脚本运行
npm run start:langgraph -- "你的指令"
# 示例
npm run start:langgraph -- "列出当前目录下的文件"
```
### 环境配置
项目需要以下环境变量:
- `DEEPSEEK_API_KEY`: DeepSeek API 密钥
- `LANGSMITH_*`: LangSmith 追踪配置(可选)
### 设计特点
1. **模块化工具设计**: 每个工具都是独立的模块,易于扩展和维护
2. **错误处理**: 所有工具都有完善的错误处理机制
3. **安全性**: 工具参数使用 Zod 进行严格验证
4. **用户体验**: 提供清晰的错误信息和操作反馈
5. **可扩展性**: 可以轻松添加新的工具到系统中
### 与 0_deepagents 项目的对比
整个项目包含两个子项目:
1. **0_deepagents**: 使用 DeepAgents 框架实现的简单代理
2. **1_langgraph**: 使用 LangGraph 框架实现的更复杂的 ReAct 代理
主要区别:
- **0_deepagents**: 使用 DeepAgents 的 FilesystemBackend,更简单但功能有限
- **1_langgraph**: 使用 LangGraph 的 StateGraph,支持复杂的状态管理和条件路由
### 项目价值
这个项目展示了如何使用 LangGraph 构建一个实用的编码代理,具有以下价值:
1. **学习价值**: 展示了 LangGraph 的核心概念和最佳实践
2. **实用价值**: 可以直接用于自动化文件操作和代码维护
3. **扩展价值**: 提供了良好的架构基础,可以轻松添加新功能
### 改进建议
1. 可以考虑添加更多工具(如 git 操作、npm 脚本执行等)
2. 可以添加更详细的日志和追踪功能
3. 可以考虑添加用户配置选项
4. 可以优化错误信息的可读性
这个项目是一个很好的 LangGraph 实践示例,展示了如何构建一个功能完整的 ReAct 风格编码代理。
同 deepagents,一样可以在 LangSmith 中查看执行日志
![]()
3. ♾️ 分钟完善 Mini Coding Agent
DLC:优化 Coding Agent
基于上述流程,我们已经完成了一个「勉强能用」的 Coding Agent
- 提供文件读写工具 ✅
- LLM + Prompt + ReAct 范式 ✅
如果说我们实现的 mca 是下限,那么上限就是 Cladue Code、甚至 OpenClaw
要实现一个上限水平的 Coding Agent,可以说是不计成本没有止境的
比较典型的优化手段如下:
- TODO 系统
- 子任务
- 允许自定义 MCP
- 优化 Prompt
- ...
我们可以继续深入,尝试接入这些优化方法
3.1. 优化前的准备:简化代码
首先基于上个章节的 tools ,重新启用一个新的子项目,这种方式创建的 Agent 「图(节点、边)」和上一章节里的除开写法以外没有任何区别,既不会像 deepagents 一样各种功能都有也不会有一堆胶水代码。非常利于我们接下来的各种优化
mca
├── .env
├── 0_deepagents
├── 1_langgraph
+├── 2_agents
+│ └── src
+│ └── index.js
└── package.json
// 2_agents/src/index.js
import "dotenv/config";
import { ChatDeepSeek } from "@langchain/deepseek";
import { createAgent } from "langchain";
import allTools from "../../1_langgraph/src/tools/index.js";
// 项目根目录
const PROJECT_ROOT = process.cwd();
// ReAct Coding Agent 系统提示词
const SYSTEM_PROMPT = `---
PROJECT_ROOT: ${PROJECT_ROOT}
---
You are a ReAct-style coding agent. You have access to a set of tools for interacting with the filesystem and executing commands.
When given a task:
1. Think about what you need to do
2. Use the appropriate tool to gather information or make changes
3. Observe the result
4. Repeat until the task is complete
`;
// 创建模型
const model = new ChatDeepSeek({
model: "deepseek-chat",
temperature: 0,
maxTokens: 4096,
apiKey: process.env.DEEPSEEK_API_KEY,
});
// 基于 LangChain createAgent 构建 Agent
const agent = createAgent({
model,
tools: allTools,
systemPrompt: SYSTEM_PROMPT,
});
// CLI 入口
const args = process.argv.slice(2);
if (args.length === 0) {
console.log("请输入指令");
process.exit(1);
}
const result = await agent.invoke(
{
messages: [
{
role: "user",
content: args[0],
},
],
},
{ recursionLimit: 100 },
);
// 输出最后一条消息
const lastMsg = result.messages[result.messages.length - 1];
console.log(lastMsg.content);
3.2. TODO 系统
在之前的 deepagents 执行日志中,可以观察到 TODO 相关逻辑(如果没有可以尝试执行一个比较复杂的任务,或者给任务加上必须用 TODO 工具的要求)
![]()
| 工具 | 说明 |
|---|---|
| WRITE_TODO | 即时更新已完成的 To-do 项,要求添加或更新未来的 To-do 项,「确保」 Agent 按照正确的顺序和步骤执行任务,同时也避免跳过步骤。 |
TODO 工具其实非常容易实现,可以理解为工具就是直接透传返回输入,每次对于 TODO 的更新都是全量的。
同时也不会有所谓的 READ_TODO 工具,原因是 WRITE_TODO 本身就是一次模型对话中的 TOOL_CALL,所以完整的入参(这也是不单独更新某一项的好处),其实就等于输出
底层逻辑是定期在对话中插入 TODO 结构文本,用于提高模型的注意力避免模型过于发散
这里我们直接用现成的 LangChain TODO 中间件即可,无需单独实现一个工具
// 2_agents/src/index.js
import "dotenv/config";
import { ChatDeepSeek } from "@langchain/deepseek";
+ import { createAgent, todoListMiddleware } from "langchain";
import allTools from "../../1_langgraph/src/tools/index.js";
// 项目根目录
const PROJECT_ROOT = process.cwd();
// ReAct Coding Agent 系统提示词
const SYSTEM_PROMPT = `---
PROJECT_ROOT: ${PROJECT_ROOT}
---
You are a ReAct-style coding agent. You have access to a set of tools for interacting with the filesystem and executing commands.
When given a task:
1. Think about what you need to do
2. Use the appropriate tool to gather information or make changes
3. Observe the result
4. Repeat until the task is complete
`;
// 创建模型
const model = new ChatDeepSeek({
model: "deepseek-chat",
temperature: 0,
maxTokens: 4096,
apiKey: process.env.DEEPSEEK_API_KEY,
});
// 基于 LangChain createAgent 构建 Agent
const agent = createAgent({
model,
tools: allTools,
systemPrompt: SYSTEM_PROMPT,
+ middleware: [todoListMiddleware()],
});
// CLI 入口
const args = process.argv.slice(2);
if (args.length === 0) {
console.log("请输入指令");
process.exit(1);
}
const result = await agent.invoke(
{
messages: [
{
role: "user",
content: args[0],
},
],
},
{ recursionLimit: 100 },
);
// 输出最后一条消息
const lastMsg = result.messages[result.messages.length - 1];
console.log(lastMsg.content);
3.3. 子任务
子任务其实就是一个独立上下文、基本同主任务一致的一次对话任务
使用子任务最大的好处是可以单独处理复杂的子任务而不消耗额外的主链路上下文
![]()
按通常实现,把子任务(Sub Agent)当成一个工具用即可
出入参、使用逻辑同样可以参考 Claude
Task
Tool name: Task
```ts
type AgentInput = {
description: string;
prompt: string;
subagent_type: string;
model?: "sonnet" | "opus" | "haiku";
resume?: string;
run_in_background?: boolean;
max_turns?: number;
name?: string;
team_name?: string;
mode?: "acceptEdits" | "bypassPermissions" | "default" | "dontAsk" | "plan";
isolation?: "worktree";
}
```
Launches a new agent to handle complex, multi-step tasks autonomously.
## Task
Launch a new agent to handle complex, multi-step tasks autonomously.
The Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
Available agent types and the tools they have access to:
- general-purpose: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)
- statusline-setup: Use this agent to configure the user's Claude Code status line setting. (Tools: Read, Edit)
- Explore: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions. (Tools: All tools except Task, ExitPlanMode, Edit, Write, NotebookEdit)
- Plan: Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs. (Tools: All tools except Task, ExitPlanMode, Edit, Write, NotebookEdit)
When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
When NOT to use the Task tool:
- If you want to read a specific file path, use the Read or Glob tool instead of the Task tool, to find the match more quickly
- If you are searching for a specific class definition like "class Foo", use the Glob tool instead, to find the match more quickly
- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Task tool, to find the match more quickly
- Other tasks that are not related to the agent descriptions above
Usage notes:
- Always include a short description (3-5 words) summarizing what the agent will do
- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
- You can optionally run agents in the background using the run_in_background parameter. When an agent runs in the background, you will be automatically notified when it completes — do NOT sleep, poll, or proactively check on its progress. Continue with other work or respond to the user instead.
- **Foreground vs background**: Use foreground (default) when you need the agent's results before you can proceed — e.g., research agents whose findings inform your next steps. Use background when you have genuinely independent work to do in parallel.
- Agents can be resumed using the `resume` parameter by passing the agent ID from a previous invocation. When resumed, the agent continues with its full previous context preserved. When NOT resuming, each invocation starts fresh and you should provide a detailed task description with all necessary context.
- When the agent is done, it will return a single message back to you along with its agent ID. You can use this ID to resume the agent later if needed for follow-up work.
- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.
- Agents with "access to current context" can see the full conversation history before the tool call. When using these agents, you can write concise prompts that reference earlier context (e.g., "investigate the error discussed above") instead of repeating information. The agent will receive all prior messages and understand the context.
- The agent's outputs should generally be trusted
- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
- If the user specifies that they want you to run agents "in parallel", you MUST send a single message with multiple Task tool use content blocks. For example, if you need to launch both a build-validator agent and a test-runner agent in parallel, send a single message with both tool calls.
- You can optionally set `isolation: "worktree"` to run the agent in a temporary git worktree, giving it an isolated copy of the repository. The worktree is automatically cleaned up if the agent makes no changes; if changes are made, the worktree path and branch are returned in the result.
Example usage:
<example_agent_descriptions>
"test-runner": use this agent after you are done writing code to run tests
"greeting-responder": use this agent to respond to user greetings with a friendly joke
</example_agent_descriptions>
<example>
user: "Please write a function that checks if a number is prime"
assistant: Sure let me write a function that checks if a number is prime
assistant: First let me use the Write tool to write a function that checks if a number is prime
assistant: I'm going to use the Write tool to write the following code:
<code>
function isPrime(n) {
if (n <= 1) return false
for (let i = 2; i * i <= n; i++) {
if (n % i === 0) return false
}
return true
}
</code>
<commentary>
Since a significant piece of code was written and the task was completed, now use the test-runner agent to run the tests
</commentary>
assistant: Now let me use the test-runner agent to run the tests
assistant: Uses the Task tool to launch the test-runner agent
</example>
<example>
user: "Hello"
<commentary>
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
</commentary>
assistant: "I'm going to use the Task tool to launch the greeting-responder agent"
</example>
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"description": {
"description": "A short (3-5 word) description of the task",
"type": "string"
},
"prompt": {
"description": "The task for the agent to perform",
"type": "string"
},
"subagent_type": {
"description": "The type of specialized agent to use for this task",
"type": "string"
},
"model": {
"description": "Optional model to use for this agent. If not specified, inherits from parent. Prefer haiku for quick, straightforward tasks to minimize cost and latency.",
"type": "string",
"enum": [
"sonnet",
"opus",
"haiku"
]
},
"resume": {
"description": "Optional agent ID to resume from. If provided, the agent will continue from the previous execution transcript.",
"type": "string"
},
"run_in_background": {
"description": "Set to true to run this agent in the background. The tool result will include an output_file path - use Read tool or Bash tail to check on output.",
"type": "boolean"
},
"max_turns": {
"description": "Maximum number of agentic turns (API round-trips) before stopping. Used internally for warmup.",
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
"isolation": {
"description": "Isolation mode. "worktree" creates a temporary git worktree so the agent works on an isolated copy of the repo.",
"type": "string",
"enum": [
"worktree"
]
}
},
"required": [
"description",
"prompt",
"subagent_type"
],
"additionalProperties": false
}
---
可以和之前一样让 CodingAgent 根据当前信息生成,不过这里为了便于理解使用一个简化的 Task Tool
// 2_agents/src/tools/task.js
import "dotenv/config";
import { ChatDeepSeek } from "@langchain/deepseek";
import { createAgent, todoListMiddleware } from "langchain";
import allTools from "../../../1_langgraph/src/tools/index.js";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// 项目根目录
const PROJECT_ROOT = process.cwd();
// ReAct Coding Agent 系统提示词
const SYSTEM_PROMPT = `---
PROJECT_ROOT: ${PROJECT_ROOT}
---
When given a task:
1. Think about what you need to do
2. Use the appropriate tool to gather information or make changes
3. Observe the result
4. Repeat until the task is complete
`;
// 创建模型
const model = new ChatDeepSeek({
model: "deepseek-chat",
temperature: 0,
maxTokens: 4096,
apiKey: process.env.DEEPSEEK_API_KEY,
});
export const taskTool = tool(
async ({ input, systemPrompt }) => {
const agent = createAgent({
model,
tools: allTools,
systemPrompt: SYSTEM_PROMPT + "\n" + systemPrompt,
middleware: [todoListMiddleware()],
});
const result = await agent.invoke(
{
messages: [
{
role: "user",
content: input,
},
],
},
{ recursionLimit: 100 },
);
const lastMsg = result.messages[result.messages.length - 1];
if (typeof lastMsg.content === "string") {
return lastMsg.content;
}
return JSON.stringify(lastMsg.content, null, 2);
},
{
name: "task",
description:
"Launches a new agent to handle complex, multi-step coding tasks autonomously. The input must include the task goal, all currently known relevant file paths and/or code snippets, any prior analysis results or summaries, plus key constraints (tech stack, style, do/don't rules, performance or security requirements) so the sub agent can work in a single shot with full context.",
schema: z.object({
input: z
.string()
.describe(
"Task description with full context for the sub agent. Include: (1) the user's goal and what needs to be done, (2) relevant file paths, modules, and/or code snippets, (3) any prior analysis results, summaries, or discovered facts that could help, and (4) important constraints such as tech stack, coding style, do/don't rules, and performance/security requirements.",
),
systemPrompt: z
.string()
.describe(
"The system prompt to be used for the sub agent. This should include all relevant context for the sub agent to work with.",
),
}),
},
);
// 2_agents/src/index.js
import "dotenv/config";
import { ChatDeepSeek } from "@langchain/deepseek";
import { createAgent, todoListMiddleware } from "langchain";
import allTools from "../../1_langgraph/src/tools/index.js";
+ import { taskTool } from "./tools/task.js";
// 项目根目录
const PROJECT_ROOT = process.cwd();
// ReAct Coding Agent 系统提示词
const SYSTEM_PROMPT = `---
PROJECT_ROOT: ${PROJECT_ROOT}
---
You are a ReAct-style coding agent. You have access to a set of tools for interacting with the filesystem and executing commands.
When given a task:
1. Think about what you need to do
2. Use the appropriate tool to gather information or make changes
3. Observe the result
4. Repeat until the task is complete
`;
// 创建模型
const model = new ChatDeepSeek({
model: "deepseek-chat",
temperature: 0,
maxTokens: 4096,
apiKey: process.env.DEEPSEEK_API_KEY,
});
+ const tools = [...allTools, taskTool];
// 基于 LangChain createAgent 构建 Agent
const agent = createAgent({
model,
+ tools,
systemPrompt: SYSTEM_PROMPT,
middleware: [todoListMiddleware()],
});
// CLI 入口
const args = process.argv.slice(2);
if (args.length === 0) {
console.log("请输入指令");
process.exit(1);
}
const result = await agent.invoke(
{
messages: [
{
role: "user",
content: args[0],
},
],
},
{ recursionLimit: 100 },
);
// 输出最后一条消息
const lastMsg = result.messages[result.messages.length - 1];
console.log(lastMsg.content);
根据其工具的入参、描述,注册到工具中之后主任务自然会在恰当的时间来调用
3.4. 自定义 MCP
一个合格 Coding Agent 当然也会允许用户自定义 MCP
MCP 本质就是工具调用,我们只需要按指定格式加载配置文件,再用对应 SDK 吧其加载成工具即可
相关文档:docs.langchain.com/oss/javascr…
// 2_agents/src/index.js
import "dotenv/config";
import { ChatDeepSeek } from "@langchain/deepseek";
import { createAgent, todoListMiddleware } from "langchain";
import allTools from "../../1_langgraph/src/tools/index.js";
import { taskTool } from "./tools/task.js";
+ import { MultiServerMCPClient } from "@langchain/mcp-adapters";
+ import mcpConfig from "../mcp.json" assert { type: "json" };
// 项目根目录
const PROJECT_ROOT = process.cwd();
// ReAct Coding Agent 系统提示词
const SYSTEM_PROMPT = `---
PROJECT_ROOT: ${PROJECT_ROOT}
---
You are a ReAct-style coding agent. You have access to a set of tools for interacting with the filesystem and executing commands.
When given a task:
1. Think about what you need to do
2. Use the appropriate tool to gather information or make changes
3. Observe the result
4. Repeat until the task is complete
`;
// 创建模型
const model = new ChatDeepSeek({
model: "deepseek-chat",
temperature: 0,
maxTokens: 4096,
apiKey: process.env.DEEPSEEK_API_KEY,
});
+ const loadCustomMcp = async () => {
+ const client = new MultiServerMCPClient(mcpConfig.mcpServers);
+ const tools = await client.getTools();
+ return tools;
+ };
+ const tools = [...allTools, taskTool, ...(await loadCustomMcp())];
// 基于 LangChain createAgent 构建 Agent
const agent = createAgent({
model,
tools,
systemPrompt: SYSTEM_PROMPT,
middleware: [todoListMiddleware()],
});
// CLI 入口
const args = process.argv.slice(2);
if (args.length === 0) {
console.log("请输入指令");
process.exit(1);
}
const result = await agent.invoke(
{
messages: [
{
role: "user",
content: args[0],
},
],
},
{ recursionLimit: 100 },
);
// 输出最后一条消息
const lastMsg = result.messages[result.messages.length - 1];
console.log(lastMsg.content);
可以配置一个任何 MCP 来验证(比如我用的高德地图 MCP)
![]()
3.5. 优化 Prompt
Prompt 看起来好像没有什么技术含量,但是实际上非常关键,并且由于不存在幂等性,难以标准化。所以我们直接参考 Claude!
里面包含了各种细节,包括不限于什么时候应该调用什么工具、如何遵循规范...
通过查看 Claude 的 Prompt,还可以非常直观的发现 mca 距离一个完善的 Coding Agent 差多远(也推荐大家找专门逆向 Claude 的文章来学习)
- 网络搜索
- 长记忆
- 适配不同情况的 Sub Agent
- Plan 模式
- 像用户提问获取必要信息
- ...
4. 结语
尽管当前许多AI工具已经实现了「开箱即用」的便捷性,但对于程序员而言,深入理解这些工具背后的原理,无疑能够更加高效和灵活地运用它们。
12-主题|内存管理@iOS-Option与内存优化技术
本文介绍与内存相关的几类优化与极限管理:Option/位运算共用内存(多选项共用一个整数)、内存的极限管理(低内存策略与约束)、Copy-on-Write(写时拷贝)、Tagged Pointer。与 01-内存五大分区、11-深浅拷贝与内存、07-实践与常见问题 配合阅读。
一、Option 与位运算共用内存
1.1 概念
- 将多个布尔或选项压缩到一个整数的不同位上,通过位运算读写,实现「多个开关/状态共占一块内存」;在 C/OC 中常用 NS_OPTIONS、位域(bitfield),在 Swift 中对应 OptionSet。
-
内存:N 个独立
BOOL或bool可能占 N 个字节(甚至对齐后更多);用一个整型的若干位表示,只需 1 个整型(如 4 或 8 字节),在选项较多或实例数量巨大时显著节省内存并利于缓存。
1.2 NS_OPTIONS / 位运算示例(Objective-C)
// 多个「选项」共用一个整型,每位表示一种开关
typedef NS_OPTIONS(NSUInteger, ViewOptions) {
ViewOptionsNone = 0,
ViewOptionsHidden = 1 << 0, // 1
ViewOptionsDisabled = 1 << 1, // 2
ViewOptionsSelected = 1 << 2, // 4
ViewOptionsLoading = 1 << 3, // 8
};
// 使用:一个 NSUInteger 存下所有选项
ViewOptions opts = ViewOptionsHidden | ViewOptionsSelected;
// 判断
BOOL isHidden = (opts & ViewOptionsHidden) != 0;
// 置位 / 清位
opts |= ViewOptionsLoading;
opts &= ~ViewOptionsDisabled;
- 上述
ViewOptions只占 一个 NSUInteger(8 字节),即可表示 64 个独立布尔;若用 64 个BOOL属性,会占用更多内存且不利于缓存。
1.3 位域(bitfield)共用内存
// 结构体内用位域:多个成员共占一个或多个整型
struct PackedFlags {
unsigned int visible : 1; // 1 bit
unsigned int enabled : 1;
unsigned int selected : 1;
unsigned int loading : 1;
unsigned int reserved : 4; // 预留
}; // 整体可仅占 4 字节(一个 unsigned int)
- 多个成员共享同一整型内存,适合配置、状态、权限等密集布尔/小范围枚举,在大量实例(如 Cell、配置项)时减少内存占用。
1.4 典型场景
| 场景 | 说明 |
|---|---|
| UI 状态 | 如 hidden、enabled、selected、loading 等用 NS_OPTIONS 或 OptionSet 存为一个整数 |
| 权限/能力 | 读、写、执行等用位表示,一个整数表示一组权限 |
| 配置/特性开关 | 大量配置项用位域或 OptionSet,减少结构体/对象体积 |
| 网络/解析标志 | 协议中的 flags 字段,多位表示多种含义,共用内存 |
二、内存的极限管理
2.1 目标与场景
- 在内存紧张(低内存设备、后台、系统压力大)时,通过主动释放、限制缓存、延迟加载等手段,把占用控制在系统允许范围内,避免被系统杀掉或 OOM。
- 与 07-实践与常见问题 中的「内存警告」「音视频/图层场景」配合使用。
2.2 策略要点
| 策略 | 说明 | |
|---|---|---|
| 响应 didReceiveMemoryWarning | 释放可重建的缓存(图片、数据)、释放非当前页大对象;主线程不阻塞,异步释放。 | |
| 缓存上限与淘汰 | 图片/数据缓存设置 maxCount / maxCost,LRU 等淘汰;避免无界增长。 | |
| 后台释放 | 进入后台时释放非必要资源(解码器、大缓冲、预览图),回到前台再按需加载。 | |
| 按需加载 / 流式 | 大列表、大文件不一次性进内存;分页、流式读取、大图降采样。 | |
| @autoreleasepool | 循环中大量临时对象用 @autoreleasepool {} 控制峰值,见 [05-AutoreleasePool与RunLoop](05-主题 |
内存管理@iOS-AutoreleasePool与RunLoop.md)。 |
| 内存映射 | 大文件用 mmap 等映射访问,减少常驻 RSS;注意映射大小与释放时机。 |
2.3 极限下的注意
- 不保留可重建数据:能重新下载、重新计算的就不要在内存里常驻。
- 控制单页/单模块占用:列表、相册、音视频播放等设定上限,避免单场景吃满内存。
- Instruments:用 Allocations、VM Tracker、Leaks 做「极限场景」压测(反复进入退出、后台、低内存模拟),观察峰值与泄漏。
三、Copy-on-Write(写时拷贝)
3.1 概念
- Copy-on-Write(COW):多个逻辑上的「副本」在未修改前共享同一份底层存储;仅在某一方发生写操作时才为该方复制出一份新存储,再修改,从而避免「一赋值就整块拷贝」的开销。
- 与深浅拷贝的关系:浅拷贝是「多引用、共享子对象」;COW 是「多引用、共享存储,写时才真正拷贝」,在保证值语义的前提下减少内存与 CPU 消耗。详见 11-深浅拷贝与内存。
3.2 Swift 中的 COW
- Array、Dictionary、Set、String 等值类型在 Swift 标准库中实现了 COW:赋值时不立即复制底层 buffer,而是共享;首次发生写操作时,若检测到 buffer 被多处引用(非唯一引用),则先复制 buffer 再写。
- 实现要点:内部持有一个引用类型的 buffer;写前通过
isKnownUniquelyReferenced(或等价机制)判断是否唯一引用,若不唯一则 copy buffer 再写。 - 效果:大量「只读共享」的赋值与传参几乎零拷贝成本;只有写时才付出拷贝代价,适合读多写少的集合与字符串。
3.3 与内存的关系
- 省内存:未修改的「副本」不占额外存储,仅多一个指向同一 buffer 的引用。
- 写时峰值:在共享的 buffer 上首次写入会触发一次拷贝,此时有短暂的内存与 CPU 开销;若写非常频繁且共享多,需注意是否适合用 COW 结构。
- 自定义值类型:Swift 不会自动为自定义 struct 实现 COW,若需要需自己维护「内部引用 + 写时复制」逻辑。
3.4 流程图(概念)
flowchart LR
A[赋值/传参] --> B{写操作?}
B -->|否| C[继续共享 buffer]
B -->|是| D{唯一引用?}
D -->|是| E[直接写]
D -->|否| F[复制 buffer 再写]
四、Tagged Pointer
4.1 概念
- Tagged Pointer 是 Apple 在 64 位 架构下的一种优化:把「小对象」的数据与类型信息直接编码进指针值本身,而不在堆上分配对象;该「指针」并不是指向堆地址,而是即是指针也是数据。
- 内存:不占用堆,不参与引用计数(retain/release 对 Tagged Pointer 为 no-op);仅占一个指针宽度(8 字节),无额外分配、无 isa、无引用计数块,极限节省小对象的内存与调用开销。
4.2 原理(64 位简要)
- 64 位下对象指针通常 16 字节对齐,低 4 位恒为 0;系统用最高位或最低位(依平台而定,如 ARM64 常用最高位)作为 tag,表示「这是 Tagged Pointer」。
- 其余位中:若干位表示类型(如 NSNumber、NSString、NSDate 等),其余位存数据(如小整数、短字符串的编码)。
- 运行时通过「解 tag + 类型 + 数据位」还原出逻辑上的「对象」,不访问堆,不触发 retain/release。
4.3 典型类型与约束
| 类型 | 说明 |
|---|---|
| NSNumber | 小整数、部分浮点数可直接存进指针,不分配堆对象。 |
| NSString | 较短字符串(如 ASCII 或少量字符)在较新系统上可能用 Tagged Pointer;更长则仍为堆上分配。 |
| NSDate 等 | 部分小对象类型在系统实现中可能使用 Tagged Pointer。 |
- 约束:能编码进指针的数据量有限(几十 bit),仅适用于「小」数据;大数、长字符串、复杂对象仍走普通堆分配。
4.4 对内存管理的影响
- 无堆分配:Tagged Pointer 不占堆,不增加 Allocations 中的对象数。
- 无引用计数:对 Tagged Pointer 发 retain/release 会被识别并忽略,不会造成过度释放或泄漏(从引用计数角度)。
-
不可假设地址:不能把 Tagged Pointer 当普通指针做指针运算或与 C 内存接口混用;判断是否 Tagged Pointer 可用运行时 API(如
objc_isTaggedPointer)。
4.5 小结对比
| 维度 | 普通堆对象 | Tagged Pointer |
|---|---|---|
| 存储位置 | 堆 | 指针值本身(无堆) |
| 引用计数 | 有 | 无(no-op) |
| 内存占用 | 对象头 + 实例 + 指针 | 仅 8 字节指针 |
| 适用 | 任意对象 | 小数据(小整数、短字符串等) |
五、思维导图
mindmap
root((Option 与内存优化))
Option 位运算
NS_OPTIONS OptionSet
位域 共用整型
内存极限管理
内存警告 缓存上限
后台释放 按需加载
CopyOnWrite
写时复制 共享 buffer
Swift 集合 isKnownUniquelyReferenced
Tagged Pointer
小对象编码进指针
无堆 无引用计数
参考文献
11-主题|内存管理@iOS-深浅拷贝与内存
本文介绍 浅拷贝(Shallow Copy) 与 深拷贝(Deep Copy) 的含义、在 Objective-C / Foundation 中的表现、与内存的关系(引用计数、新对象分配、共享与独立),以及 NSCopying、属性 copy、Swift 值类型与写时拷贝。前置知识见 03-引用计数与MRC详解、04-ARC详解。
一、浅拷贝与深拷贝的定义
1.1 概念
| 类型 | 含义 | 内存上的表现 |
|---|---|---|
| 浅拷贝 | 只复制「当前这一层」:得到一个新对象(新指针),但对象内部的元素/子对象仍指向原有的实例。 | 新对象占用新内存(新引用计数);内部元素不复制,多出一份对原元素的引用(引用计数 +1)。 |
| 深拷贝 | 递归复制整棵对象树:当前对象及其内部所有引用到的对象都重新创建一份。 | 整棵对象树都占用新内存,拷贝前后完全独立,无共享引用。 |
- 单层对象(如 NSString、NSData):浅拷贝与深拷贝在「是否共享内容」上的差异,取决于类型是否可变、实现是否共享底层存储(如 copy 后可能共享 buffer,仅引用计数 +1)。
-
集合类(NSArray、NSDictionary 等):浅拷贝 = 新容器 + 元素仍指向原元素;深拷贝 = 新容器 + 对每个元素再递归 copy,需自行实现或使用
initWithArray:copyItems:YES等 API。
1.2 与内存、引用计数的关系
- 浅拷贝:生成一个新对象(容器或包装类),该对象对内部子对象的引用会使这些子对象的引用计数 +1;子对象本身不复制,内存上共享子对象。
- 深拷贝:生成全新的对象图,每个被拷贝的对象都有新内存、新引用计数;原对象与拷贝无共享,释放一方不影响另一方。
二、Foundation 中的 copy 与 mutableCopy
2.1 常见类型的拷贝语义(概览)
| 类型 | copy | mutableCopy | 说明 |
|---|---|---|---|
| NSString | 不可变副本(可能共享存储,引用计数 +1) | NSMutableString | 不可变 → 不可变 多为浅拷贝;不可变 → 可变 会分配新缓冲 |
| NSMutableString | 不可变 NSString(新内存) | NSMutableString(浅拷贝) | copy 得到不可变,防止外部修改 |
| NSArray | 浅拷贝,新数组、元素仍指向原元素 | NSMutableArray(浅拷贝) | 元素引用计数 +1,元素本身不复制 |
| NSDictionary | 浅拷贝 | NSMutableDictionary(浅拷贝) | 同上 |
| NSData | 浅拷贝(可能共享字节) | NSMutableData | 实现可能共享底层 buffer |
| 自定义类 | 由 copyWithZone: 实现决定 |
由 mutableCopyWithZone: 决定 |
可做浅拷贝或深拷贝 |
- 上述「浅拷贝」指:容器是新对象,元素仍是原对象引用;对容器增删不影响对方,对元素内容的修改可能影响对方(若元素可变)。
2.2 集合的「单层深拷贝」
-
[[NSArray alloc] initWithArray:array copyItems:YES]:会向每个元素发送copy,得到新数组 + 一层新元素;若元素本身是集合,其内部不会递归 copy,因此是单层深拷贝,不是递归深拷贝。 - 真正递归深拷贝需自己实现或使用序列化(如
NSKeyedArchiver)再反序列化,注意性能与内存。
三、NSCopying 与 NSMutableCopying
3.1 协议
-
NSCopying:实现
- (id)copyWithZone:(NSZone *)zone;调用[obj copy]时最终走copyWithZone:。 -
NSMutableCopying:实现
- (id)mutableCopyWithZone:(NSZone *)zone;调用[obj mutableCopy]时走mutableCopyWithZone:。
3.2 拷贝与内存管理
- ARC:copy/mutableCopy 返回的对象由调用方持有(引用计数 +1),遵循 ARC 规则。
-
MRC:返回的对象为调用方拥有,需在适当时机 release 或 autorelease;在
copyWithZone:里返回的对象应为 +1 所有权(alloc 或 copy 出来的)。
3.3 自定义类的浅拷贝与深拷贝示例(概念)
// 浅拷贝:新对象,但 property 仍指向原对象(retain/copy 使引用计数 +1)
- (id)copyWithZone:(NSZone *)zone {
MyClass *copy = [[MyClass allocWithZone:zone] init];
copy.name = self.name; // 若 name 是 copy 属性,会 [self.name copy]
copy.child = self.child; // 若 child 是 strong,仅 retain,共享同一 child
return copy;
}
// 深拷贝:递归复制子对象
- (id)copyWithZone:(NSZone *)zone {
MyClass *copy = [[MyClass allocWithZone:zone] init];
copy.name = [self.name copy];
copy.child = [self.child copy]; // 子对象也 copy,完全独立
return copy;
}
- 选择浅拷贝还是深拷贝取决于业务:共享子对象可省内存但需注意多线程/可变性;完全独立则省心但内存与耗时更大。
四、属性的 copy 与内存
4.1 copy 属性
- 声明为
@property (copy) NSString *name时,setter 会对传入值调用 copy,即持有的是「传入对象的拷贝」的所有权;若传入的是 NSMutableString,拷贝后得到不可变 NSString,避免外部在别处修改导致当前实例被意外改动。 - 对 Block 使用 copy 属性:Block 的 copy 会把栈 Block 拷贝到堆(见 10-Block内存管理),并持有该堆 Block;与「深浅拷贝」中的「拷贝」语义不同,但都涉及「新对象 + 引用计数」。
4.2 深浅拷贝与属性
- 若属性是 集合(如 NSArray),用 copy 只是对集合本身做浅拷贝(新容器、元素仍共享);若希望「外部传入的数组」与内部完全隔离,要么接受浅拷贝(元素共享),要么在 setter 里做一层
initWithArray:copyItems:YES或自定义深拷贝,并注意内存与性能。
五、内存注意与选型
5.1 浅拷贝
| 优点 | 缺点 |
|---|---|
| 省内存、速度快 | 与原对象共享子对象;若子对象可变,一边修改会影响另一边;多线程需额外同步 |
5.2 深拷贝
| 优点 | 缺点 |
|---|---|
| 完全独立,无共享,线程安全更易控制 | 内存与 CPU 开销大,递归深拷贝需防循环引用与栈溢出 |
5.3 何时用哪种
-
浅拷贝:只关心「多一份容器引用」、元素共享可接受(或元素不可变)时;Foundation 的
copy/mutableCopy默认多为浅拷贝(容器层)。 -
深拷贝:需要「完全独立副本」、避免外部修改或跨线程共享可变状态时;可单层深拷贝(
copyItems:YES)或自定义递归深拷贝。
六、流程图:浅拷贝与深拷贝的内存关系(概念)
flowchart TB
subgraph 浅拷贝
A1[原容器] --> A2[新容器]
A1 --> A3[元素a]
A2 --> A3
end
subgraph 深拷贝
B1[原容器] --> B2[新容器]
B1 --> B3[元素a]
B2 --> B4[元素a 的副本]
end
七、Swift 中的「拷贝」与内存
7.1 值类型与引用类型
- 值类型(struct、enum、基础类型):赋值与传参是拷贝语义(复制一份值);从「不共享同一块堆对象」的角度看,更像「深拷贝」。
-
引用类型(class):赋值与传参是引用,不产生新对象,仅多一个指针;若要独立副本需显式实现拷贝(如实现
NSCopying或自定义copy()方法)。
7.2 写时拷贝(Copy-on-Write)
- Array、Dictionary、Set 等是值类型,但底层存储可能共享 buffer;修改时才复制一份(Copy-on-Write),既保证值语义又减少不必要的内存与拷贝开销。
- 与 OC 的「浅拷贝」不同:Swift 集合的「拷贝」在未修改前可能共享存储,修改时再分配新内存,由标准库保证语义正确。COW 原理、Swift 实现要点(如
isKnownUniquelyReferenced)及与内存的关系见 12-Option与内存优化技术 中的「Copy-on-Write」一节。
八、思维导图:深浅拷贝与内存
mindmap
root((深浅拷贝与内存))
概念
浅拷贝 新对象 共享元素
深拷贝 新对象 递归复制
引用计数
浅拷贝 元素 rc+1
深拷贝 全新对象图
Foundation
copy mutableCopy
集合 copyItems
NSCopying
copyWithZone
自定义浅/深拷贝
属性 copy
setter 调 copy
Block NSString
Swift
值类型 拷贝语义
CopyOnWrite
九、参考文献
10-主题|内存管理@iOS-Block内存管理
本文专门介绍 Objective-C Block 与 Swift 闭包 的内存管理:Block 的三种类型(全局/栈/堆)、捕获变量与内存、copy 语义、循环引用 与破除,以及作为属性/参数时的注意点。前置知识见 04-ARC详解、06-weak与循环引用。
一、Block 是什么(与内存的关系)
- Block 是 Apple 对 C 语言扩展的闭包:可捕获外部变量、作为对象参与引用计数;在内存上既包含代码(函数指针),也包含捕获的变量(结构体形式),因此既有「存在位置」(栈/堆/全局)也有「对捕获对象的持有关系」。
- 内存管理 需关注两点:Block 对象本身 的分配与释放(栈 block / 堆 block / 全局 block),以及 Block 对捕获变量(尤其是 OC 对象) 的强引用/弱引用,避免循环引用与泄漏。
二、Block 的三种类型与内存位置
2.1 类型与存储位置
| 类型(运行时 isa) | 存储位置 | 产生条件(典型) |
|---|---|---|
| NSGlobalBlock | 全局区(.data/.text) | 未捕获任何外部变量(或仅捕获全局/静态变量) |
| NSStackBlock | 栈 | 捕获了自动变量(局部变量),且未 copy 到堆(MRC 下常见) |
| NSMallocBlock | 堆 | 对栈 block 执行 copy,或 ARC 下多数「需要逃逸」的 block 被编译器自动 copy 到堆 |
- 全局 Block:不依赖栈帧,无需 copy,可当作单例使用。
- 栈 Block:随栈帧销毁而失效,若要在作用域外使用(如存为属性、异步回调),必须先 copy 到堆;ARC 下编译器会在赋值给 strong/copy 属性、跨函数传递等场景自动插入 copy。
- 堆 Block:参与引用计数,由 ARC/MRC 管理;copy 时引用计数 +1,release 时 -1。
2.2 简单判断示例(ARC)
// 无捕获 → 全局 Block(__NSGlobalBlock__)
void (^gBlock)(void) = ^{ NSLog(@"no capture"); };
// 捕获局部变量 → 栈 Block(__NSStackBlock__),若赋给 strong/copy 属性则会被 copy 成堆 Block
int a = 1;
void (^sBlock)(void) = ^{ NSLog(@"%d", a); };
// 赋值给 copy/strong 属性或作为参数传给需要「持有」的 API 时,会变成 __NSMallocBlock__
2.3 MRC 下 Block 的 copy 必要性
- 在 MRC 下,栈上的 Block 在函数返回后栈帧被回收,若此时 block 已被传给调用方或存到堆对象(如属性),再执行会野指针/未定义行为。
- 因此 MRC 下:凡是需要跨作用域保留的 block,必须对其执行一次 copy,将栈 block 拷贝到堆上,得到 NSMallocBlock,之后按普通 OC 对象做 retain/release;用完后要对堆 block 做 release(或 autorelease)。
-
ARC 下:编译器在「赋值给 strong/copy 属性、作为参数传给会保留 block 的 API」等场景自动插入 copy,一般无需手写
[block copy]。
三、Block 捕获变量与内存
3.1 捕获方式概览
| 捕获对象/变量 | 默认行为(OC 对象) | 对引用计数的影响 |
|---|---|---|
| 局部 OC 对象(自动变量) | 强引用(strong) | Block 被 copy 到堆时,会 retain 被捕获的对象;block 释放时 release |
| 局部标量(int、结构体等) | 值拷贝 | 不涉及引用计数 |
| __block 修饰的变量 | 生成结构体,block 与外部共享 | 若 __block 变量指向 OC 对象,需注意 MRC/ARC 下 retain 行为;__block 可改写 |
| __weak 修饰的对象 | 弱引用 | Block 不持有该对象,不增加引用计数,可避免循环引用 |
3.2 对象捕获与循环引用
- Block 若强引用了某个对象 A(如直接使用
self),而 A 又强引用了该 block(如 block 被 A 的 strong/copy 属性持有),则形成 self → block → self 的循环,两者都不会释放。 -
解决:在 block 外使用
__weak typeof(self) wself = self,block 内使用wself,这样 block 对 self 是弱引用;若在 block 执行过程中担心 self 被释放,可在 block 内再用__strong typeof(wself) sself = wself强引用一次(仅限 block 执行期),避免执行到一半 self 被置 nil。详见 06-weak与循环引用。
3.3 __block 与内存(简述)
- __block 使局部变量在 block 内可被修改,编译器会生成一个包装结构,block 捕获的是该结构;若 __block 变量指向 OC 对象,在 ARC 下通常不会造成 block 对对象的强引用(对象存在 __block 结构里),但若在 block 内给该变量赋新值,会涉及旧值 release、新值 retain。MRC 下 __block 不会自动 retain 对象,需自行管理。
- 历史上用 __block 打破循环(__block self + block 内置 nil)的写法在 ARC 下不推荐,应使用 __weak 打破循环。
四、Block 作为属性、参数与返回值
4.1 属性声明
| 属性修饰 | 说明 |
|---|---|
| copy | 设值时对 block 执行 copy;MRC 时代推荐,ARC 下 strong 与 copy 对 block 效果类似(都会 copy 到堆),习惯上仍常用 copy 表达「这是 block」的语义。 |
| strong | ARC 下与 copy 类似,赋值时也会把栈 block copy 到堆并强引用。 |
- Block 属性应避免用 assign(栈 block 离开作用域后失效,会野指针)。
4.2 作为参数与返回值
- 作为参数:若 API 会保存 block(如延迟执行、存入数组),API 内部应对传入的 block 做 copy(或由 ARC 在传入时保证是堆 block);调用方传栈 block 时,由被调用方 copy 到堆是常见约定。
- 作为返回值:返回 block 时,若希望调用方在函数返回后仍能使用,应返回堆上的 block(MRC 下 return 前对 block 做 copy/autorelease;ARC 下编译器会根据返回类型自动处理)。
五、ARC 与 MRC 下 Block 内存小结
| 场景 | MRC | ARC |
|---|---|---|
| 栈 block 需跨作用域使用 | 必须对 block 执行 copy,用完后 release | 编译器在赋值给 strong/copy、传参等场景自动 copy |
| Block 属性 | 用 copy,setter 里对 block copy、对旧 block release | copy 或 strong 均可,都会导致 copy 到堆并强引用 |
| Block 内引用 self | 避免 self→block→self:用 __weak 或 __block+置 nil | 用 __weak self,必要时 block 内 __strong 一次 |
| Block 捕获的 OC 对象 | copy 到堆时 block 会 retain 捕获的对象;block release 时 release 这些对象 | 同左,由编译器插入 |
六、流程图:Block 从创建到释放(概念)
flowchart LR
A[定义 Block] --> B{是否捕获自动变量?}
B -->|否| C[__NSGlobalBlock__ 全局]
B -->|是| D[__NSStackBlock__ 栈]
D --> E[赋值给 strong/copy 或 传参]
E --> F[copy 到堆 __NSMallocBlock__]
F --> G[Block 被 release]
G --> H[对捕获对象 release]
七、Swift 闭包与内存
- Swift 闭包 与 OC Block 语义对应:闭包会捕获外部变量,默认对类对象是强引用。
-
循环引用:若对象强引用闭包,闭包内又使用了
self(或捕获了 self),则形成循环;解决方式为在闭包捕获列表中写[weak self]或[unowned self](后者在 self 一定不会先于闭包释放时使用,否则会野指针)。 - @escaping:标记闭包会「逃逸」出当前函数(如异步回调),编译器会按需将闭包拷贝到堆上,与 OC 中「block 被 copy 到堆」对应。
八、思维导图:Block 内存管理知识结构
mindmap
root((Block 内存管理))
三种类型
全局 Block 无捕获
栈 Block 捕获未 copy
堆 Block copy 后
捕获与引用
对象默认强引用
__weak 破循环
__block 可改写
属性与生命周期
copy/strong 属性
MRC 需手写 copy
ARC 自动 copy
循环引用
self → block → self
weak self strong self
九、参考文献
A股三大指数收盘涨跌不一,“三桶油”集体涨停
09-主题|内存管理@iOS-Category与关联对象内存管理
本文介绍 Objective-C Category(分类) 与内存的关系,以及通过 关联对象(Associated Objects) 在 Category 中「挂载」数据时的内存管理:关联策略(policy)、释放时机、循环引用与最佳实践。前置知识见 04-ARC详解、06-weak与循环引用。
一、Category 与内存的关系
1.1 Category 是什么
- Category 用于在不修改原类的前提下,为已有类添加方法(以及通过关联对象间接添加「属性」式的存储)。
- Category 不能直接添加实例变量(ivar),因此不会改变类实例的内存布局与 sizeof;实例大小由原类及其子类的 ivar 决定。
1.2 对内存管理的影响
| 维度 | 说明 |
|---|---|
| 实例大小 | Category 不增加实例占用,无需从「对象体积」角度做特殊内存管理。 |
| 方法实现 | Category 中的方法若创建或持有对象,仍遵循 ARC/MRC 规则(谁持有谁释放、避免循环引用)。 |
| 「属性」存储 | 若在 Category 中通过 关联对象 模拟属性,则关联的 value 的持有方式 由 association policy 决定,需正确设置以避免泄漏或野指针。 |
下文重点说明关联对象的内存语义与使用注意。
二、关联对象(Associated Objects)简述
2.1 作用
- 在不增加 ivar 的前提下,把键值对绑在某个对象上:主对象被释放时,运行时会自动释放其关联的 value(按 policy 做 release 等)。
- 常用于在 Category 中为已有类添加「存储型属性」、或为任意对象挂载扩展数据。
2.2 API(Objective-C 运行时)
// 设置:object 为主对象,key 为键,value 为值,policy 为关联策略
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// 获取
id objc_getAssociatedObject(id object, const void *key);
// 移除(将 value 设为 nil 即可,会按 policy 释放原 value)
objc_setAssociatedObject(object, key, nil, policy);
三、关联策略(policy)与内存管理
3.1 常用策略对照表
| 策略常量 | 语义(对 value 的持有方式) | 适用场景 |
|---|---|---|
| OBJC_ASSOCIATION_RETAIN | 强引用(retain),主对象释放时对 value release | 普通 OC 对象属性(类似 strong) |
| OBJC_ASSOCIATION_COPY | 拷贝后强引用(copy),主对象释放时对拷贝 release | 字符串、block 等需拷贝的类型 |
| OBJC_ASSOCIATION_ASSIGN | 不持有(assign),主对象释放时不对 value 做 release | 基本类型、或「弱引用」场景(注意野指针) |
| OBJC_ASSOCIATION_RETAIN_NONATOMIC | 同 RETAIN,非原子 | 性能敏感、不需原子性时 |
| OBJC_ASSOCIATION_COPY_NONATOMIC | 同 COPY,非原子 | 同上 |
3.2 与 ARC 属性修饰符的对应
| 若属性声明为 | 关联时建议 policy |
|---|---|
| strong(对象) | OBJC_ASSOCIATION_RETAIN |
| copy(block/NSString) | OBJC_ASSOCIATION_COPY |
| assign / weak | OBJC_ASSOCIATION_ASSIGN(assign 不保证置 nil,若为对象有野指针风险;true weak 需运行时支持,关联对象常用 ASSIGN 存 weak 包装或非持有) |
3.3 释放时机
-
主对象 dealloc 时,运行时会自动对所有关联的 value 按各自 policy 执行 release(或等效操作),无需在 dealloc 里手动
objc_setAssociatedObject(..., nil, ...)或单独 release。 - 若在业务上希望提前解除某条关联,可主动
objc_setAssociatedObject(object, key, nil, policy),原 value 会按 policy 被释放。
四、Category 中「属性」的常见写法与内存
4.1 强引用存储(RETAIN)
// Category 中为 NSObject 添加一个“强引用”属性
static const void *kMyKey = &kMyKey;
- (void)setMyProperty:(id)obj {
objc_setAssociatedObject(self, kMyKey, obj, OBJC_ASSOCIATION_RETAIN);
}
- (id)myProperty {
return objc_getAssociatedObject(self, kMyKey);
}
- 内存:set 时对新 value retain、对旧 value release;主对象 dealloc 时自动 release 当前 value,无泄漏。
-
注意:若
myProperty内部又强引用主对象(如 block 捕获 self),会循环引用,需用 weak 打破(见下)。
4.2 拷贝存储(COPY,如 block)
- (void)setMyBlock:(void (^)(void))block {
objc_setAssociatedObject(self, kBlockKey, block, OBJC_ASSOCIATION_COPY);
}
- Block 常用 COPY,与属性
copy一致;主对象释放时会对拷贝的 block release。
4.3 弱引用 / 不持有(ASSIGN)与循环引用
- 若用 OBJC_ASSOCIATION_ASSIGN 存一个对象指针,主对象不会持有该对象;但主对象 dealloc 时不会把该指针置 nil,若外部未持有,可能产生野指针。
- 循环引用:主对象 A 通过 RETAIN 关联了对象 B,B 又强引用了 A → 双方都不释放。解决办法:让 B 对 A 使用 weak(若 B 是自定义类可改);或 A 不通过 RETAIN 关联 B,改用 ASSIGN + 弱引用包装(需注意生命周期与野指针)。
- Category 中若「属性」是 delegate 或会反向引用 self 的对象,应避免用 RETAIN 持有该对象,可考虑 ASSIGN 存 weak 包装或不在 Category 里存该引用。
五、流程图:关联对象生命周期
flowchart LR
A[主对象存在] --> B[setAssociatedObject value policy]
B --> C[value 被 retain/copy 等]
A --> D[主对象 dealloc]
D --> E[运行时按 policy 释放所有关联 value]
E --> F[value 引用计数减一 或 置空]
六、小结与最佳实践
| 场景 | 建议 |
|---|---|
| Category 中存普通 OC 对象 | 使用 OBJC_ASSOCIATION_RETAIN(或 RETAIN_NONATOMIC)。 |
| Category 中存 block / 需拷贝类型 | 使用 OBJC_ASSOCIATION_COPY。 |
| 不持有、仅赋值指针(如 delegate) | 可用 OBJC_ASSOCIATION_ASSIGN,注意主对象释放后不置 nil,避免野指针。 |
| 避免循环引用 | 不在 Category 中用 RETAIN 关联「会强引用主对象」的对象;或对方对主对象使用 weak。 |
| 释放 | 主对象 dealloc 时关联会自动清理,一般无需在 dealloc 里手动移除。 |
参考文献
08-主题|内存管理@iOS-内存对齐
本文介绍 内存对齐(Memory Alignment) 的概念、为何需要对齐、结构体内存对齐 的规则与示例,以及在 iOS/ARM64 下的典型约定。与「内存五大分区」中数据在栈、堆、全局区的布局密切相关,见 01-主题|内存管理@iOS-内存五大分区。
一、什么是内存对齐
1.1 定义
- 内存对齐:数据在内存中的起始地址满足一定约束,通常是「地址为自身所占字节数的整数倍」(或按平台规定的对齐值)。
- 例如:4 字节的
int在多数平台上需** 4 字节对齐**(地址为 4 的倍数);8 字节的double需 8 字节对齐(地址为 8 的倍数)。
1.2 为什么需要对齐
| 原因 | 说明 |
|---|---|
| CPU 访问效率 | 许多 CPU 对未对齐访问有性能惩罚或需多次总线访问;对齐后可按固定步长、单次或更少次数访问。 |
| 硬件与 ABI 要求 | ARM、x86 等架构对某些类型有对齐要求;未对齐访问在部分平台可能触发异常(如 ARM 未对齐访问可配置为 fault)。 |
| 以空间换时间 | 通过填充(padding) 满足对齐,会多占一些字节,但换来稳定、高效的访问。 |
二、基本类型的对齐(典型值)
以下为 64 位 iOS/ARM64 下常见类型的典型对齐与大小(具体以 ABI 与编译器为准):
| 类型 | 大小(字节) | 典型对齐(字节) |
|---|---|---|
| char / bool | 1 | 1 |
| short | 2 | 2 |
| int | 4 | 4 |
| long / 指针(64 位) | 8 | 8 |
| float | 4 | 4 |
| double | 8 | 8 |
| long double | 8 或 16 | 8 或 16 |
平台约定:iOS 64 位(ARM64)下,编译器常采用 8 字节 作为结构体整体对齐的上限之一(即结构体大小与起始地址常为 8 的倍数);32 位下多为 4 字节。
三、结构体内存对齐规则
3.1 三条常见规则
- 成员对齐:结构体第一个成员的偏移为 0;后续成员的起始偏移 = 该成员自身对齐值的整数倍,不足则插入 padding。
- 嵌套结构体:若成员是结构体,该成员的起始偏移 = 其内部最大成员对齐值的整数倍(即嵌套结构体按自身「最严格」对齐要求对齐)。
- 整体对齐:结构体的总大小 = 其内部最大成员对齐值的整数倍;末尾不足则补足,以便结构体数组时每个元素仍对齐。
3.2 流程图:计算结构体布局(伪流程)
flowchart TB
A[遍历每个成员] --> B[当前偏移 是 该成员对齐的整数倍?]
B -->|否| C[补 padding 到满足]
B -->|是| D[放置该成员]
C --> D
D --> E[偏移 += 成员大小]
E --> A
F[所有成员放完] --> G[总大小 是 最大成员对齐的整数倍?]
G -->|否| H[末尾补 padding]
G -->|是| I[得到 sizeof]
H --> I
四、示例:结构体大小与 padding
4.1 C / Objective-C 示例
// 假设 64 位:指针 8 字节、int 4 字节、char 1 字节
struct Example1 {
double a; // 8 字节,偏移 0,[0-7]
char b; // 1 字节,偏移 8,[8]
int c; // 4 字节,需 4 对齐,故偏移 12,[12-15]
short d; // 2 字节,偏移 16,[16-17]
}; // 最大成员对齐 8,总大小需 8 的倍数:18 → 24,末尾补 6 字节
// sizeof(Example1) == 24
| 成员 | 大小 | 对齐 | 起始偏移 | 说明 |
|---|---|---|---|---|
| a | 8 | 8 | 0 | 第一个成员 |
| b | 1 | 1 | 8 | 无 padding |
| c | 4 | 4 | 12 | 偏移 9、10、11 不满足 4 对齐,补 3 字节 |
| d | 2 | 2 | 16 | 无 padding |
| (尾部) | — | — | 18→24 | 总大小凑成 8 的倍数 |
4.2 成员顺序对大小的影响
同一批成员、顺序不同会导致 padding 不同,从而总大小不同:
struct Compact {
double a; // 0-7
int b; // 8-11
int c; // 12-15
char d; // 16
}; // 总大小 17 → 对齐 8 → 24 字节(末尾补 7)
struct Sparse {
char a; // 0
double b; // 需 8 对齐 → 8-15,前补 7
int c; // 16-19
}; // 总大小 20 → 对齐 8 → 24 字节
实践建议:若需节省结构体占用,可将大类型放前、小类型集中,减少中间 padding。
五、Swift 中的内存布局与对齐
5.1 MemoryLayout
- MemoryLayout<T>.size:类型 T 的实际占用字节数(不含尾部为数组元素对齐而留的 padding)。
- MemoryLayout<T>.stride:在连续存储(如数组)中,相邻两个 T 的起始地址之差,即「对齐后的大小」。
- MemoryLayout<T>.alignment:类型 T 的对齐要求(字节数)。
5.2 示例
struct SHPerson {
var age: Int // 8 字节
var weight: Int // 8 字节
var sex: Bool // 1 字节
}
// size = 17(实际成员占用)
// stride = 24(8 字节对齐后,用于数组等)
// alignment = 8
六、与内存五大分区的关系
- 栈、堆、全局区中存放的局部变量、对象、全局/静态变量,其起始地址与内部成员都受对齐约束;编译器与运行时在分配时会保证对齐。
- 理解对齐有助于:估算结构体/类实例占用、排查「sizeof 与预期不符」、与 C 互操作或做底层布局时避免未对齐访问。
七、自定义对齐与 packed(简述)
| 手段 | 说明 |
|---|---|
| _attribute_((aligned(n))) | 指定变量或结构体按 n 字节对齐(如缓存行 64 字节)。 |
| _attribute_((packed)) | 取消结构体内部 padding,成员紧挨排列;可减小体积但可能未对齐,访问效率或安全性下降,需谨慎使用。 |
八、小结(思维导图)
mindmap
root((内存对齐))
目的
CPU 访问效率
ABI 与硬件要求
规则
成员按自身对齐
整体大小为最大对齐的整数倍
iOS/ARM64
常用 8 字节整体对齐
size 与 stride
实践
成员顺序影响大小
packed / aligned 慎用
参考文献
- ARM Architecture Reference Manual(对齐与未对齐访问)
- Swift - MemoryLayout
- ABI 文档:ARM64 下 iOS/macOS 的 C / Objective-C 类型大小与对齐
07-主题|内存管理@iOS-实践与常见问题
本文在 01~06 基础上,汇总 内存警告、Instruments 排查与泄漏分析、Timer 管理、野指针、音视频与图层场景 等实践要点,以及常见问题与最佳实践。建议先掌握总纲与 ARC、weak 等再阅读本文;Timer 与 NSProxy 见 06-weak与循环引用。
一、内存警告(Memory Warning)
1.1 机制
- 系统在内存紧张时向应用发送 UIApplication 内存警告(如
didReceiveMemoryWarning);若不释放非必要缓存,系统可能终止进程。
1.2 响应建议
| 做法 | 说明 |
|---|---|
| 释放缓存 | 图片缓存、数据缓存等可重建的,在收到警告时清理或缩小 |
| 释放不可见资源 | 非当前页的大图、大模型等可延迟重新加载的,可先释放 |
| 不阻塞主线程 | 释放与重建尽量异步,避免卡顿 |
1.3 回调示例(ViewController)
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// 释放可重建的缓存、大图等
}
二、内存泄漏(Leak)排查
2.1 常见原因
- 循环引用:对象成环,引用计数永不为 0 → 用 weak 打破。
- 定时器/观察者未移除:NSTimer、KVO、Notification 等强引用 target/observer,未在 dealloc 前移除 → 及时 invalidate/removeObserver。
-
Block/闭包强引用:block 强引用 self 且 self 强引用 block →
[weak self];Block 类型与 copy 语义见 10-Block内存管理。 -
Category 关联对象:用
objc_setAssociatedObject时若用 RETAIN 关联了「会强引用主对象」的对象(如 block 捕获 self),会形成循环引用;应避免或改用 weak 打破。详见 09-Category与关联对象内存管理。
2.2 Instruments(Leaks / Allocations)
- Leaks:检测进程内已无法被引用到的「泄漏」内存块。
- Allocations:查看各对象分配与存活情况,结合 Generations 或 Mark Generation 观察某操作后是否持续增长不降。
- 结合 Call Tree 与源码,定位泄漏对象与引用链。
2.3 内存泄漏的内存分析(进阶)
- 堆快照与对比:在 Allocations 中多次 Mark Generation(如进入页面前 Mark、返回后再 Mark),对比两次快照的「Persistent」对象数量与大小,找出本应释放却仍存活的对象。
- VM 区域:在 Allocations 的 Statistics 或 VM Tracker 中查看各 VM 区域(如 CG image、Image IO、IOSurface、Audio 等),定位是哪类内存持续增长(如解码图、音视频缓冲未释放)。
- 引用链分析:对疑似泄漏对象右键 Show in Memory Graph 或查看 Reference History,看清「谁在持有它」的引用链,从而找到应改为 weak 或应 invalidate/remove 的持有方。
- Malloc Stack / Call Tree:开启 Malloc Stack(Allocations 模板或 Edit Scheme → Diagnostics)可看到分配时的调用栈,便于确认泄漏对象来自哪段代码;Call Tree 的「Invert Call Tree」「Hide System」可快速聚焦业务代码。
- Leaks 与 Allocations 配合:Leaks 只报「不可达」的泄漏;很多「仍被错误持有」的对象不会报 Leak,需用 Allocations 的 Generation 对比 + 引用链分析。
2.4 Timer 管理(详细)
- NSTimer 会强引用 target,且 RunLoop 会持有 timer;若 VC 强引用 timer 且 target 是 self,则 VC → timer → self 形成循环,VC 不会 dealloc。
-
解决:① 在 dealloc 前 invalidate(若循环未破,dealloc 不会被调用,故须先破环);② 用 NSProxy 弱引用 self 作为 timer 的 target,使 timer 强引用的是 proxy 而非 VC,详见 06-weak与循环引用 中的「NSProxy 与 Weak、Timer 管理」;③ iOS 10+ 使用 block 版
scheduledTimerWithTimeInterval:repeats:block:,block 内用 weak self,timer 不直接强引用 self。 -
CADisplayLink 同样强引用 target,需用相同思路(proxy 或 block 若可用)并在 dealloc 里
invalidate。
三、野指针与崩溃
3.1 成因
- 对象已 release/dealloc,仍有指针访问该内存 → 野指针;再次向该对象发消息或访问成员易 EXC_BAD_ACCESS 等崩溃。
3.2 预防
| 手段 | 说明 |
|---|---|
| ARC + weak | 使用 weak 时,对象释放后指针自动置 nil,发送消息无效果但不会崩溃 |
| 不重复 release(MRC) | MRC 下严格配对,避免对同一对象多次 release |
| 置空指针 | 释放后将指针置 nil(ARC 中 weak 自动完成) |
四、音视频场景内存注意
- 解码缓冲与采样缓冲:音视频解码会产生 CVPixelBuffer、CMSampleBuffer 等,若不及时释放或重复堆积,会快速推高内存;播放/渲染完或不再需要时应及时释放,避免在回调或队列中积压。
- 大文件/流:避免一次性将整段音视频读入内存;使用 AVAssetReader、流式读取 等按需加载,及时释放已解码帧或已播放的缓冲。
- 后台与生命周期:进入后台时释放非必要解码器、清空大缓冲或暂停解码,回到前台再重建,可配合 UIApplication 后台通知 与 didReceiveMemoryWarning。
- 循环引用:在 AVFoundation 回调、block 中若使用 self,需 weak self,避免 VC 或播放器持有 block 且 block 强引用 self 导致不释放。
- CVPixelBuffer / 图像缓冲:渲染或处理完及时 CVPixelBufferRelease(若自己 retain 过)或交给系统回收;避免在缓存中无上限保留未释放的 buffer。
内存极限管理(缓存上限、后台释放、按需加载、内存映射等)见 12-Option与内存优化技术 中的「内存的极限管理」一节。
五、图层处理场景内存注意
- 图片解码与尺寸:UIImage 在赋值给 UIImageView 或绘制前会解码为位图,大图会占用 宽×高×4 字节 量级内存;应对大图做降采样(如用 Image I/O 或 Core Graphics 按显示尺寸解码),或使用缩略图/裁剪,避免全尺寸解码多张大图。
- CALayer 与 backing store:图层有内容(如 contents、drawRect)时会有 backing store 占用内存;离屏渲染(圆角+裁剪、阴影、group opacity 等)会生成额外离屏缓冲,多而大时会增加内存与 GPU 压力,可适当减少离屏层或用位图缓存。
-
离屏渲染与缓存:
shouldRasterize = YES会缓存光栅化结果,图层复杂或尺寸大时缓存会占内存;在不需要时关闭或缩小 layer bounds。 - 大图列表:列表(UITableView/UICollectionView)中大量大图时,做好 复用、按需加载 与 内存警告时释放;可配合 didReceiveMemoryWarning 清空图片缓存。
- Core Graphics / 位图:自己创建的 CGContext、CGBitmapContext 在不用时 CGContextRelease;UIGraphics 的 context 若为自己创建需对应释放。
六、最佳实践小结
| 场景 | 建议 | |
|---|---|---|
| 属性默认 | 对象类型用 strong;delegate/dataSource 用 weak | |
| Block | 若 block 被 self 持有且 block 内用 self,用 weak self;block 内若需保证执行期 self 存活,可再 strong 一次;Block 属性用 copy/strong,详见 [10-Block内存管理](10-主题 | 内存管理@iOS-Block内存管理.md) |
| 定时器/通知 | 在 dealloc 前 invalidate timer、removeObserver,避免强引用导致不释放 | |
| 大量临时对象 | 循环内使用 @autoreleasepool 控制峰值 | |
| 内存警告 | 实现 didReceiveMemoryWarning,释放可重建缓存 | |
| Category 关联对象 | 用 OBJC_ASSOCIATION_RETAIN/COPY 存对象、OBJC_ASSOCIATION_COPY 存 block;避免关联会强引用主对象的对象以防循环引用,详见 [09-Category与关联对象内存管理](09-主题 | 内存管理@iOS-Category与关联对象内存管理.md) |
| 集合/对象拷贝 | 区分浅拷贝(新容器、元素共享)与深拷贝(完全独立);属性 copy 对集合仅浅拷贝,需完全隔离时考虑深拷贝或 copyItems,详见 [11-深浅拷贝与内存](11-主题 | 内存管理@iOS-深浅拷贝与内存.md) |
| Timer | 用 NSProxy 弱引用 self 作 target 破循环,或 iOS 10+ 用 block 版 API;dealloc 前必须 invalidate,详见 [06-weak与循环引用](06-主题 | 内存管理@iOS-weak与循环引用.md) |
| 音视频 | 及时释放解码/采样缓冲,流式加载大文件,后台释放非必要资源,回调中用 weak self | |
| 图层/大图 | 大图降采样、控制离屏渲染与 rasterize 缓存、列表复用与按需加载、CGContext 及时释放 |
七、流程图:泄漏排查思路
flowchart LR
A[怀疑泄漏] --> B[Instruments Leaks]
B --> C[看引用链]
C --> D[查循环引用/未移除的观察者等]
D --> E[weak/移除/改设计]
参考文献
沪深京三市成交额突破3万亿
06-主题|内存管理@iOS-weak与循环引用
本文介绍 weak(弱引用) 的语义、在运行时中的实现思路(SideTable/weak_table)、循环引用 的成因与破除方式,以及 block、delegate 等场景下的注意点。ARC 基础见 04-ARC详解。Block 的三种类型、copy 与捕获变量见 10-Block内存管理。
一、weak 的语义
1.1 定义
- weak:不增加对象的引用计数,不拥有对象;当对象被释放时,所有指向它的 weak 指针会被自动置为 nil,避免野指针。
- 与 strong 对比:strong 持有对象(rc+1),strong 不释放则对象不 dealloc;weak 不持有,对象可被其他引用释放,释放后 weak 自动置 nil。
1.2 使用场景
| 场景 | 说明 |
|---|---|
| 打破循环引用 | A → B → A,将其中一条边改为 weak,避免双方都无法释放 |
| 非拥有关系 | delegate、dataSource 等,通常用 weak,由外部持有生命周期 |
| block 内引用 self | 使用 [weak self] 避免 self → block → self 循环 |
二、循环引用(Retain Cycle)
2.1 成因
- 循环引用:对象 A 强引用 B,B 又强引用 A(或经过多条边回到 A),形成环;双方引用计数都不为 0,永远无法 dealloc,造成泄漏。
2.2 常见情形与破除
| 情形 | 破除方式 |
|---|---|
| 两个对象互相 strong | 一方改为 weak(如 child 对 parent 用 weak) |
| self → block → self | block 内用 [weak self],必要时内部再 strong 一次避免提前释放 |
| delegate 双方都 strong | 通常 delegate 属性声明为 weak,由外部持有 |
| Timer 强引用 target | VC 强引用 timer,timer 强引用 target(即 VC)→ 循环;用 NSProxy 弱引用 VC 作为 timer 的 target,或 iOS 10+ 用 block 版 API |
2.3 Block 中 weak self 示例(Objective-C)
__weak typeof(self) wself = self;
self.block = ^{
__strong typeof(wself) sself = wself; // 避免 block 执行过程中 self 被释放
if (!sself) return;
[sself doSomething];
};
三、NSProxy 与 Weak、Timer 管理
3.1 NSTimer 的循环引用问题
-
NSTimer 会强引用其 target;若 target 是 VC(或任意对象 A),且 A 又强引用了该 timer(如
self.timer),则形成 A → timer → target(A) 的循环,A 与 timer 都不会释放。 - 仅在 A 里用
__weak self给 timer 的 target 传参无效:timer 内部保存的是传入的 target 指针并对其强引用,不会因为调用方用 weak 而改为弱引用。
3.2 用 NSProxy 打破 Timer 循环引用
- 思路:让 timer 的 target 不是一个强引用 self 的对象,而是一个中间对象;该中间对象对 self 只持 weak,并把 timer 的回调转发给 self。这样引用关系为:VC → timer → proxy(弱引用 VC),VC 释放时 proxy 的 weak 置 nil,proxy 可随之释放;timer 需在 VC 的 dealloc 里 invalidate,或由 proxy 在转发时发现 target 为 nil 时 invalidate(视实现而定)。
- NSProxy 是专门做「转发」的根类,不继承自 NSObject,实现 forwardInvocation: 与 methodSignatureForSelector:,把消息转给 weak 持有的 target 即可;内存上 proxy 只多一个 weak 指针,不增加 target 的引用计数。
3.3 WeakProxy 示例(Objective-C)
@interface WeakProxy : NSProxy
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation WeakProxy
+ (instancetype)proxyWithTarget:(id)target {
WeakProxy *p = [WeakProxy alloc];
p.target = target;
return p;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
// 使用:timer 强引用的是 proxy,proxy 只 weak 引用 self
WeakProxy *proxy = [WeakProxy proxyWithTarget:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(onTick) userInfo:nil repeats:YES];
// dealloc 中仍须 [self.timer invalidate],否则 RunLoop 仍持有 timer
3.4 Timer 管理要点小结
| 要点 | 说明 |
|---|---|
| invalidate | VC(或持有 timer 的对象)dealloc 前必须调用 [timer invalidate],否则 RunLoop 持有 timer,timer 又强引用 target,导致泄漏或野指针。 |
| block 版 API(iOS 10+) |
+[NSTimer scheduledTimerWithTimeInterval:repeats:block:] 的 block 里用 [weak self],timer 不直接强引用 self,可避免 timer→self 的强引用;仍需在 dealloc 里 invalidate。 |
| 子线程 | 子线程 RunLoop 默认不跑,timer 需加到 RunLoop 并 run;线程结束时记得 invalidate。 |
四、weak 实现思路(简述)
4.1 全局 weak 表
- 运行时维护全局的 weak 表(与对象地址关联):记录「哪些 weak 指针正在指向该对象」。
- 当对象 dealloc 时,查该表,把表中所有 weak 指针置为 nil,再销毁对象。
4.2 SideTable 与 weak_table(概念)
- 为减少锁竞争,常用 SideTable 分片:根据对象地址映射到某一张 SideTable;每张表内有 weak_table,存「对象 → 指向它的 weak 指针列表」。
- storeWeak 等函数:在注册 weak 指针、对象释放时更新对应 SideTable 中的 weak 表。
4.3 流程图:对象释放时 weak 置 nil
flowchart TB
A[对象 dealloc] --> B[查 weak 表]
B --> C[遍历指向该对象的 weak 指针]
C --> D[将每个 weak 指针置为 nil]
D --> E[销毁对象]
五、思维导图小结
mindmap
root((weak 与循环引用))
weak 语义
不增加引用计数
对象释放时置 nil
循环引用
成环 无法释放
破除 一方改 weak
Block
weak self
strong self 防提前释放
NSProxy 与 Timer
Timer 强引用 target
WeakProxy 转发 破循环
实现
SideTable weak_table
dealloc 时清空 weak
参考文献
- About Memory Management - Practical Memory Management
- objc4 源码:
objc-weak.mm、NSObject.mm(storeWeak、weak_clear等)
05-主题|内存管理@iOS-AutoreleasePool与RunLoop
本文介绍 自动释放池(AutoreleasePool) 的原理、底层结构(AutoreleasePoolPage)、与 RunLoop 的协作关系,以及对象何时被批量 release。引用计数基础见 03-引用计数与MRC详解。
自动释放池是什么(简要介绍)
自动释放池(AutoreleasePool) 是用于延迟释放对象的机制:当对象收到 autorelease 时,不会立即让引用计数 -1,而是被加入当前线程的自动释放池;当池被 pop/drain 时,池会对其中所有对象统一发送 release,从而在「某一时刻」批量 -1。在 MRC 下需手写 autorelease;在 ARC 下由编译器在需要时自动插入。主线程的 RunLoop 在每次循环开始会 push 一个池、在休眠或退出前 pop 该池,因此主线程上的 autorelease 对象多在「本次事件处理结束」时被释放。子线程若无 RunLoop,应显式使用 @autoreleasepool { } 控制释放时机,避免临时对象堆积。
一、AutoreleasePool 的作用
1.1 为什么需要
- autorelease 表示「稍后再 release」:不立刻 -1,而是把对象交给当前自动释放池,由池在某一时刻统一对池内对象发送 release。
- 作用:延迟释放,避免在密集创建临时对象的场景下频繁立刻 release,可将多次 release 合并到池 drain 时执行,有利于性能与局部性。
1.2 与 RunLoop 的关系(主线程)
- 主线程 RunLoop 在一次循环中会:
- 进入时:push 一个 AutoreleasePool;
- 休眠/退出前:pop 该池,即对池内所有对象执行 release(drain)。
- 因此,主线程上没有显式
@autoreleasepool时,当前 RunLoop 迭代结束前创建的 autorelease 对象,会在本次迭代末尾被批量释放。
二、@autoreleasepool 语法与底层
2.1 语法
@autoreleasepool {
// 池内创建的 autorelease 对象,在 } 时统一 release
id obj = [SomeObject createObject]; // 若返回 autorelease 对象
}
// 池 pop,obj 收到 release
2.2 底层对应(伪代码)
-
@autoreleasepool { ... }编译后等价于:- 入口:
objc_autoreleasePoolPush()(入栈一个哨兵/边界); - 出口:
objc_autoreleasePoolPop()(pop 到该边界,对之间加入的对象依次 release)。
- 入口:
2.3 AutoreleasePoolPage(简述)
- 自动释放池由 AutoreleasePoolPage 组成的栈结构实现;每页约 4KB,存若干对象指针。
- push 时可能新开一页或复用当前页;pop 时从栈顶向栈底对每个对象 release,直到遇到对应 push 的边界。
三、释放时机小结
| 场景 | 释放时机 |
|---|---|
| 主线程、无显式 @autoreleasepool | 当前 RunLoop 迭代结束前(休眠/退出时 pop 顶层池) |
| 显式 @autoreleasepool { } | 离开 } 时 pop,池内对象立即被 release |
| 子线程 | 若没有 RunLoop 或未手动加池,需在线程中显式 @autoreleasepool,否则 autorelease 对象可能堆积到线程退出 |
四、流程图:RunLoop 与 AutoreleasePool 协作(主线程)
flowchart LR
subgraph RunLoop 一次迭代
A[进入] --> B[Push Pool]
B --> C[处理事件]
C --> D[休眠/退出前]
D --> E[Pop Pool]
E --> F[池内对象 release]
end
五、应用场景
-
循环中大量创建临时对象:在循环内层包一层
@autoreleasepool { },每轮迭代结束即释放,避免峰值过高。 -
子线程中创建大量 autorelease 对象:在线程入口或循环内使用
@autoreleasepool,避免只依赖线程退出才释放。
参考文献
南充市安智创科低空经济发展公司成立,注册资本1亿元
04-主题|内存管理@iOS-ARC详解
本文介绍 ARC(Automatic Reference Counting,自动引用计数) 的机制、strong/weak/unowned 等所有权修饰符、编译器如何插入引用计数代码,以及常见应用场景与注意事项。前置知识见 03-引用计数与MRC详解。
ARC 是什么(简要介绍)
ARC 即 Automatic Reference Counting:在编译期由编译器根据代码中的所有权修饰符(如 strong、weak)和代码结构,自动插入 retain、release、autorelease 等调用,开发者不再手写这些方法。底层仍然使用与 MRC 相同的引用计数规则,只是「谁在何时 +1/-1」由编译器决定。ARC 自 iOS 5 / WWDC 2011 引入,现为 Objective-C 与 Swift 的推荐方式;Swift 仅支持 ARC。使用 ARC 时仍需理解强引用与弱引用、循环引用及 自动释放池 的释放时机,见 05-AutoreleasePool与RunLoop、06-weak与循环引用。
一、ARC 是什么
1.1 定义
- ARC 是编译期特性:编译器根据所有权修饰符与代码结构,在合适位置自动插入 retain、release、autorelease 等调用。
- 与 MRC 使用同一套引用计数规则,对象生命周期语义一致;开发者不再手写 retain/release,减少遗漏与错误。
1.2 与 MRC 对比
| 维度 | MRC | ARC |
|---|---|---|
| 谁写 retain/release | 开发者手写 | 编译器自动插入 |
| 所有权表达 | 通过方法名约定 + 手写调用 | 通过变量/属性修饰符(strong/weak 等) |
| autorelease | 手写 autorelease | 编译器在需要时插入 |
| 循环引用 | 需手写 weak 或打破引用 | 同样需用 weak/unowned 打破 |
二、所有权修饰符(Objective-C)
2.1 常见修饰符
| 修饰符 | 含义 | 引用计数影响 |
|---|---|---|
| __strong(默认) | 强引用,拥有对象 | 赋值时 retain,离开作用域或置 nil 时 release |
| __weak | 弱引用,不拥有对象 | 不增加引用计数;对象释放时自动置为 nil |
| __unsafe_unretained | 不保留引用,不拥有 | 不增加引用计数;对象释放后不置 nil,可能野指针 |
| __autoreleasing | 通过引用传入并在 autorelease 池中释放 | 用于 out 参数等场景 |
2.2 属性与修饰符对应
| 属性声明 | 默认修饰符 | 说明 | |
|---|---|---|---|
strong |
__strong | 强引用,常用 | |
weak |
__weak | 弱引用,打破循环或非拥有关系 | |
copy |
__strong(拷贝语义) | 设值时 copy,用于 block、NSString 等;深浅拷贝与 copy 语义见 [11-深浅拷贝与内存](11-主题 | 内存管理@iOS-深浅拷贝与内存.md) |
assign |
__unsafe_unretained | 不持有,多用于基本类型或需避免循环时(非对象慎用) |
三、ARC 下的典型场景
3.1 强引用与释放时机
// 局部变量:离开作用域时自动 release
- (void)foo {
NSObject *obj = [[NSObject alloc] init]; // 强引用,rc=1
// 使用 obj
} // 作用域结束,编译器插入 release,obj 可能 dealloc
3.2 弱引用与循环引用
// 两个对象互相强引用 → 循环引用,都无法释放
// 解决:一方改为 weak
@interface Child : NSObject
@property (nonatomic, weak) Parent *parent; // 弱引用父类
@end
3.3 Block 中的循环引用
// self → block → self,形成循环
__weak typeof(self) wself = self;
self.block = ^{
__strong typeof(wself) sself = wself;
[sself doSomething];
};
详见 06-weak与循环引用。Block 的三种类型(全局/栈/堆)、copy 语义与 MRC/ARC 差异见 10-Block内存管理。
四、流程图:ARC 编译期插入示意
flowchart TB
subgraph 源码
A[strong 赋值]
B[变量离开作用域]
end
subgraph 编译器插入
A --> C[插入 retain]
B --> D[插入 release]
end
五、Swift 中的 ARC
- Swift 仅支持 ARC,无 MRC。
-
strong(默认)、weak、unowned 与 OC 语义对应;闭包捕获列表
[weak self]/[unowned self]用于避免循环引用。 - 详见 Swift 官方 - Automatic Reference Counting。