阅读视图

发现新文章,点击刷新页面。

第 15 课:会话管理 — 上下文、模型与持久化

所属阶段:第三阶段「工作流实战」(第 12-20 课) 前置条件:第 10-11 课(Hooks 系统、脚本层) 本课收获:能根据任务选模型,能在三种动态上下文间切换


一、本课概述

前面几课聚焦于"怎么写代码"。本课聚焦于一个同样重要但常被忽视的问题:怎么管理你的 AI 助手

AI 编程助手有两个关键限制:

  1. 上下文窗口有限 — 不是无限记忆,信息会被挤出去
  2. 模型选择影响成本和质量 — 用错模型要么太贵要么太弱

本课回答三个问题:

  1. 上下文窗口怎么管理? — 安全区 vs 危险区,Strategic Compact
  2. 模型怎么选? — 三层模型策略
  3. 会话状态怎么持久化? — 三件套 Hook 系统

学完本课,你将能根据任务类型选择合适的模型,管理上下文窗口避免信息丢失,并在会话之间保持工作连续性。


二、上下文窗口管理

2.1 安全区与危险区

AI 编程助手的上下文窗口不是一个均匀的空间。越接近上限,模型的表现越不稳定:

┌────────────────────────────────────────────────┐
│              上下文窗口(200K tokens)            │
│                                                 │
│  ┌───────────────────────────────────────────┐  │
│  │          安全区 0% - 80%                   │  │
│  │                                           │  │
│  │  ✓ 多文件重构                              │  │
│  │  ✓ 功能实现(跨文件)                       │  │
│  │  ✓ 复杂调试                                │  │
│  │  ✓ 架构讨论                                │  │
│  │                                           │  │
│  ├───────────────────────────────────────────┤  │
│  │          危险区 80% - 100%                 │  │
│  │                                           │  │
│  │  △ 仅限单文件编辑                          │  │
│  │  △ 独立工具创建                            │  │
│  │  △ 文档更新                                │  │
│  │  △ 简单 Bug 修复                           │  │
│  │  ✗ 不要做多文件重构                         │  │
│  │  ✗ 不要做复杂调试                           │  │
│  └───────────────────────────────────────────┘  │
└────────────────────────────────────────────────┘

关键规则:避免在上下文窗口的最后 20% 做复杂任务。

当你感觉 AI 开始"忘记"前面讨论过的内容、重复已经做过的事情、或者答非所问,很可能是上下文窗口接近满了。

2.2 MCP 的上下文陷阱

这是一个很多人不知道的陷阱:

启用超过 10 个 MCP 服务器时,有效上下文从 200K 压缩到约 70K。

每个 MCP 服务器的工具定义会占用上下文空间。10 个 MCP 各带 10 个工具 = 100 个工具定义,每个定义几百 tokens,光是工具声明就占去了大量空间。

MCP 数量 有效上下文 建议
0-3 个 ~200K 安全
4-7 个 ~150K 注意
8-10 个 ~100K 警告
>10 个 ~70K 危险 — 优先用 CLI + Skills 替代

解决方案

  • 优先使用 CLI 工具和 Skills 替代 MCP
  • 只启用当前任务需要的 MCP
  • 使用 /context-budget 命令监控上下文使用情况

2.3 Strategic Compact — 手动压缩

当上下文接近 80% 时,你有两个选择:

  1. 自动压缩 — 系统自动丢弃"不重要"的信息(不可控,可能丢失关键上下文)
  2. Strategic Compact — 在逻辑断点手动触发压缩(可控,保留关键信息)
自动压缩(被动):
  上下文满了 → 系统自动裁剪 → 可能丢失你需要的信息 → 质量下降

Strategic Compact(主动):
  完成一个逻辑阶段 → 手动压缩 → 保留关键结论 → 开始下一阶段

最佳时机

  • 完成一个功能的 TDD 循环后
  • 代码审查结束,修复所有问题后
  • 切换到不同的功能/文件时
  • 讨论完架构方案、做出决策后

核心原则:Strategic Compact 优于自动压缩。在每个逻辑断点主动压缩,而不是等到系统被迫压缩。


三、模型分层选择

3.1 三层模型表

ECC 在 performance.md 中定义了三层模型选择策略:

模型 定位 能力占比 成本 适用场景
Haiku 4.5 轻量高频 Sonnet 的 90% 最低(Sonnet 的 1/3) 高频调用的 Worker Agent、Pair Programming、代码生成
Sonnet 4.6 主力开发 最佳编程模型 中等 日常开发、编排多 Agent、复杂编码
Opus 4.5 深度推理 最强推理 最高 架构决策、复杂分析、研究任务

3.2 选择决策树

任务到来
    │
    ├─ 是否需要深度推理/架构决策?
    │   └─ 是 → Opus 4.5
    │
    ├─ 是否是日常开发/代码审查/编排?
    │   └─ 是 → Sonnet 4.6
    │
    └─ 是否是高频轻量任务/Worker Agent?
        └─ 是 → Haiku 4.5

3.3 ECC Agent 中的模型选择

看看 ECC 的 Agent 是怎么选模型的:

Agent Model 原因
planner opus 规划需要最强推理能力
architect opus 架构决策需要深度分析
tdd-guide sonnet 日常开发任务
code-reviewer sonnet 代码审查是常规操作
build-error-resolver sonnet 修复错误需要编码能力

关键发现:只有需要"想清楚"的 Agent 用 Opus(planner、architect),需要"做出来"的 Agent 用 Sonnet。

3.4 成本优化建议

成本 = 模型单价 × Token 消耗

降低成本的三个杠杆:
1. 模型降级:能用 Haiku 就不用 Sonnet,能用 Sonnet 就不用 Opus
2. 减少 Token:Strategic Compact、精简 Prompt、减少不必要的上下文
3. 减少 MCP:用 CLI + Skills 替代 MCP,减少工具定义的 Token 消耗

四、会话持久化三件套

ECC 通过三个 Hook 实现会话状态的持久化:

4.1 三件套概览

┌────────────────────────────────────────────────┐
│              会话持久化三件套                     │
│                                                 │
│  ┌─────────────────┐                            │
│  │ SessionEnd Hook │  会话结束时自动保存          │
│  │ "保存当前进度"    │  → 存储到 .claude/sessions/ │
│  └────────┬────────┘                            │
│           │                                     │
│           ▼                                     │
│  ┌─────────────────┐                            │
│  │SessionStart Hook│  新会话开始时自动恢复         │
│  │ "恢复上次进度"    │  ← 读取 .claude/sessions/  │
│  └────────┬────────┘                            │
│           │                                     │
│           ▼                                     │
│  ┌─────────────────┐                            │
│  │PreCompact Hook  │  压缩前自动保存快照          │
│  │ "压缩前保存"     │  → 防止压缩丢失关键信息     │
│  └─────────────────┘                            │
└────────────────────────────────────────────────┘

4.2 工作流程

会话 A:
  工作中... → 上下文接近 80%
  │
  ├─ PreCompact Hook 触发 → 保存当前状态快照
  │
  └─ 继续工作... → 会话结束
      │
      └─ SessionEnd Hook 触发 → 保存最终状态

会话 B(新的会话):
  │
  ├─ SessionStart Hook 触发 → 恢复会话 A 的状态
  │
  └─ 从上次中断的地方继续

4.3 手动命令

除了自动 Hook,你也可以手动管理会话:

命令 作用
/save-session 手动保存当前会话状态
/resume-session 恢复指定的历史会话
/sessions 查看所有保存的会话列表
/checkpoint 创建一个命名检查点(比 save-session 更精细)

最佳实践

  • 完成一个重要阶段后手动 /save-session
  • 在做有风险的操作前 /checkpoint
  • 开始新会话时用 /resume-session 恢复上下文

五、动态上下文三模式

ECC 支持根据任务类型切换不同的上下文模式。每种模式会加载不同的指令集,引导 AI 以不同的方式工作:

5.1 三种模式

模式 上下文文件 工作方式 适用场景
dev contexts/dev.md 代码优先 — 快速编码、最小讨论 功能实现、Bug 修复
review contexts/review.md 审查模式 — 仔细检查、分级报告 代码审查、PR 审查
research contexts/research.md 先理解再行动 — 广泛搜索、深入分析 技术调研、架构决策

5.2 模式对比

同一个任务在不同模式下的行为差异:

任务:"这个函数有性能问题"

模式 行为
dev 直接定位瓶颈、写优化代码、跑基准测试
review 分析代码复杂度、列出所有潜在问题、给出分级建议
research 搜索类似场景的解决方案、比较多种优化策略、写分析报告

5.3 切换时机

开始新功能 → research 模式(先理解需求和技术方案)
     │
     ▼
方案确定 → dev 模式(快速编码实现)
     │
     ▼
代码写完 → review 模式(仔细审查质量和安全)
     │
     ▼
修复问题 → dev 模式(快速修复审查发现的问题)

六、Extended Thinking

6.1 什么是 Extended Thinking

Extended Thinking 允许模型在给出回答之前进行更深入的内部推理。默认开启,预留最多 31,999 tokens 用于思考。

普通模式:
  问题 → 直接回答

Extended Thinking 模式:
  问题 → [内部推理 31,999 tokens] → 更深思熟虑的回答

6.2 控制方式

操作 方法
切换开关 Option+T(macOS)/ Alt+T(Windows/Linux)
配置文件 ~/.claude/settings.json 中设置 alwaysThinkingEnabled
预算上限 export MAX_THINKING_TOKENS=10000
查看思考过程 Ctrl+O 启用 Verbose 模式

6.3 Plan Mode

当面对复杂任务时,可以结合 Extended Thinking 使用 Plan Mode:

  1. 开启 Extended Thinking(默认已开启)
  2. 启用 Plan Mode — AI 会先输出结构化计划
  3. 多轮审查 — 让 AI 从不同角度检视计划
  4. 确认后执行

Plan Mode 特别适合:

  • 涉及多文件的重构
  • 架构变更
  • 不确定最佳方案的场景

七、综合策略表

根据任务类型,选择合适的模型、上下文模式和管理策略:

任务类型 推荐模型 上下文模式 上下文策略
架构决策 Opus 4.5 research 开启 Extended Thinking + Plan Mode
新功能开发 Sonnet 4.6 dev 每个 TDD 循环后 Strategic Compact
代码审查 Sonnet 4.6 review 审查结束后 Compact
Bug 修复 Sonnet 4.6 dev 单次会话完成
Worker Agent Haiku 4.5 dev 最小上下文,频繁调用
技术调研 Opus 4.5 research 广泛搜索,保留结论
文档更新 Haiku 4.5 dev 低上下文敏感度

八、本课练习

练习 1:模型选择判断(10 分钟)

为以下 6 个场景选择合适的模型,并说明理由:

  1. 为一个 React 组件写单元测试
  2. 设计一个微服务架构方案
  3. 批量重命名 20 个文件中的变量
  4. 审查一个包含安全敏感代码的 PR
  5. 研究是否应该从 REST 迁移到 GraphQL
  6. 修复一个 CSS 样式 Bug

练习 2:上下文管理模拟(15 分钟)

假设你正在用 Claude Code 开发一个新功能,上下文窗口已经用到 75%。你需要:

  1. 决定是否要 Strategic Compact
  2. 列出你会保留的关键信息
  3. 列出你可以安全丢弃的信息
  4. 写出 Compact 后的"恢复摘要"

练习 3:三模式对比(20 分钟)

选一段你自己写过的代码(或本课程前面练习的代码),用三种不同的上下文模式分别处理同一个改进任务:

  1. dev 模式:直接优化代码
  2. review 模式:审查并列出所有问题
  3. research 模式:研究最佳实践后给出建议

对比三种模式的输出差异。

练习 4(选做):计算 MCP 成本

列出你当前启用的所有 MCP 服务器,估算它们总共占用多少上下文空间。如果超过 10 个,找出哪些可以用 CLI + Skills 替代。


九、本课小结

你应该记住的 内容
上下文安全区 0-80% 正常工作,80-100% 仅限简单任务
Strategic Compact 在逻辑断点主动压缩,优于自动压缩
模型三层 Haiku(轻量高频)、Sonnet(主力开发)、Opus(深度推理)
MCP 陷阱 >10 个 MCP 时上下文从 200K 压到 ~70K
持久化三件套 SessionEnd 保存、SessionStart 恢复、PreCompact 压缩前保存
三种上下文模式 dev(代码优先)、review(审查模式)、research(先理解再行动)
Extended Thinking 默认开启 31,999 tokens,Option+T 切换

十、下节预告

第 16 课:多代理编排 — 并行、视角与隔离

到目前为止,我们一直在使用单个 Agent。下节课我们将学习如何让多个 Agent 协作:什么时候并行、什么时候顺序、如何用多视角分析同一个问题、如何用 Git Worktree 隔离不同 Agent 的工作区。这是 ECC 最强大也最复杂的能力。

预习建议:阅读 rules/common/agents.md,特别是 Parallel Task Execution 和 Multi-Perspective Analysis 两节。

第 14 课:验证循环 — 从代码到可提交

所属阶段:第三阶段「工作流实战」(第 12-20 课) 前置条件:第 13 课(TDD 全流程) 本课收获:一次完整通过验证循环的代码提交


一、本课概述

代码写完了、测试通过了,能提交吗?在 ECC 的世界里,答案是不能

测试通过只证明功能正确,但没有回答:代码风格规范吗?类型安全吗?有没有泄露密钥?有没有 SQL 注入?这些问题需要验证循环来回答。

本课回答三个问题:

  1. 验证循环有几步? — 四步检查 + 一步提交
  2. 每步防范什么风险? — 从逻辑错误到密钥泄露
  3. 失败了怎么办? — 修复后从头重跑,没有捷径

学完本课,你将能独立完成一次从代码到提交的完整验证流程。


二、验证循环四步

2.1 全景图

┌────────────────────────────────────────────────────────┐
                    验证循环                              
                                                         
  ┌──────┐   ┌──────┐   ┌──────────┐   ┌──────────┐    
   TEST    LINT    TYPECHECK│   SECURITY     
  │测试     │风格     │类型检查      │安全检查       
  └──┬───┘   └──┬───┘   └────┬─────┘   └────┬─────┘    
                                                    
                                                    
   PASS?      PASS?       PASS?          PASS?         
                                               
  Yes No     Yes No      Yes No         Yes No         
                                               
     └─→ 修复  从头重跑    └─→ 修复       └─→ 修复  
                                                   
                                                   
             全部 PASS  可提交                          
└────────────────────────────────────────────────────────┘

2.2 每步防范什么

步骤 检查内容 防范的风险 典型工具
TEST 单元/集成/E2E 测试 逻辑错误、边界错误、回归 Jest、Vitest、Mocha、pytest
LINT 代码风格、格式、反模式 风格不一致、潜在 Bug(未使用变量等) ESLint、Prettier、Ruff、golint
TYPECHECK 类型安全 类型不匹配、空值异常、接口不兼容 tsc、mypy、go vet
SECURITY 密钥泄露、注入、依赖漏洞 密钥泄露、SQL 注入、XSS、已知 CVE security-reviewer Agent、npm audit

2.3 为什么是这个顺序

顺序不是随意的,而是按发现成本从低到高排列:

  1. TEST 最先 — 如果逻辑都不对,检查风格毫无意义
  2. LINT 第二 — 风格问题最容易发现和修复
  3. TYPECHECK 第三 — 类型错误可能需要修改接口,影响范围较大
  4. SECURITY 最后 — 安全检查最严格,也最耗时

三、每步详解

3.1 TEST — 测试通过

这一步在第 13 课已经详细讲过。要点回顾:

# 运行所有测试
npm test

# 带覆盖率
npm test -- --coverage

# 要求:80%+ 覆盖率,所有测试 PASS

失败时:

  • 修复实现代码,不是修复测试(除非测试本身写错了)
  • 使用 tdd-guide Agent 辅助排查
  • 检查测试隔离性 — 是否有共享状态导致的耦合

3.2 LINT — 风格检查

Lint 检查代码是否符合项目约定的风格规范:

# JavaScript/TypeScript
npx eslint .

# Python
ruff check .

# Go
golangci-lint run

# Markdown(ECC 项目自身)
npx markdownlint-cli '**/*.md' --ignore node_modules

ECC 项目中的具体 Lint 配置

  • 使用 @eslint/js flat config
  • Markdown 使用 markdownlint-cli
  • 所有 Lint 检查必须在提交前通过

失败时:

  • 大部分 Lint 错误可以自动修复:npx eslint . --fix
  • 手动修复后重新运行 Lint
  • 不要用 // eslint-disable 绕过检查(除非有充分理由且加注释说明)

3.3 TYPECHECK — 类型检查

类型检查确保代码的类型安全:

# TypeScript
npx tsc --noEmit

# Python
mypy .

# Go(内置)
go vet ./...

失败时:

  • 类型错误通常意味着接口设计有问题
  • 不要用 any(TypeScript)或 # type: ignore(Python)绕过
  • 如果确实需要绕过,加注释说明原因

3.4 SECURITY — 安全检查

这是验证循环中最严格的一步。ECC 的 security.md 定义了 8 项强制安全检查:

提交前安全检查清单:
  □ 无硬编码密钥(API Key、密码、Token)
  □ 所有用户输入已验证
  □ SQL 注入防护(参数化查询)
  □ XSS 防护(HTML 已消毒)
  □ CSRF 保护已启用
  □ 认证/授权已验证
  □ 所有端点有速率限制
  □ 错误消息不泄露敏感数据

工具辅助:

# 依赖漏洞扫描
npm audit

# 密钥扫描
git diff --cached | grep -i "api_key\|secret\|password\|token"

# 使用 security-reviewer Agent
# 会自动扫描 CRITICAL 安全问题

失败时 — 安全响应协议

  1. 立即停止 — 不要继续提交
  2. 调用 security-reviewer Agent
  3. 修复所有 CRITICAL 问题
  4. 轮换任何可能已暴露的密钥
  5. 审查整个代码库是否有类似问题

四、任何一步失败 → 从头重跑

这是验证循环最重要的规则:

TEST 失败 → 修复 → 重跑 TEST → LINT → TYPECHECK → SECURITY
LINT 失败 → 修复 → 重跑 TEST → LINT → TYPECHECK → SECURITY
TYPECHECK 失败 → 修复 → 重跑 TEST → LINT → TYPECHECK → SECURITY
SECURITY 失败 → 修复 → 重跑 TEST → LINT → TYPECHECK → SECURITY

为什么修复后要从头重跑?

因为修复一个问题可能引入新的问题:

  • 修复类型错误可能破坏现有测试
  • 修复安全问题可能改变代码逻辑
  • 修复 Lint 错误可能改变代码格式影响行为

只有从头到尾全部 PASS,代码才是可提交的。


五、Conventional Commits 格式

验证循环通过后,就可以提交了。ECC 要求使用 Conventional Commits 格式:

5.1 格式

<type>: <description>

<optional body>

5.2 类型速查表

类型 用途 示例
feat 新功能 feat: add slugify utility function
fix Bug 修复 fix: handle empty string in slugify
refactor 重构(不改行为) refactor: extract regex patterns to constants
docs 文档 docs: add JSDoc for slugify function
test 测试 test: add edge case tests for slugify
chore 杂项(构建、依赖) chore: update eslint config
perf 性能优化 perf: cache compiled regex in slugify
ci CI/CD ci: add coverage check to GitHub Actions

5.3 好的提交信息 vs 差的提交信息

# BAD — 描述做了什么(what)
git commit -m "update slugify.js"

# BAD — 太笼统
git commit -m "fix bug"

# GOOD — 描述为什么这样做(why)
git commit -m "fix: handle consecutive spaces in slugify to prevent double hyphens"

# GOOD — 带正文补充上下文
git commit -m "feat: add input validation to slugify

TypeError is thrown for non-string arguments to fail fast
at the call site rather than producing unexpected results
from implicit type coercion."

六、代码质量清单

在验证循环之外,coding-style.md 还定义了一份代码质量清单。这不是自动化检查,而是人工确认

提交前代码质量清单:
  □ 代码可读、命名清晰
  □ 函数短小(<50 行)
  □ 文件聚焦(<800 行)
  □ 无深层嵌套(>4 层)
  □ 错误处理完备
  □ 无硬编码值(使用常量或配置)
  □ 无 mutation(使用不可变模式)

6.1 每项的判断标准

检查项 通过标准 常见违规
可读性 变量名表达含义,逻辑清晰 单字母变量、嵌套三元表达式
函数 <50 行 用行数计算,不含空行和注释 一个函数做太多事
文件 <800 行 200-400 行是理想范围 所有逻辑放一个文件
嵌套 <4 层 if/for/while 的嵌套深度 用 early return 拍平
错误处理 每个可能失败的操作都有处理 空 catch 块、忽略 Promise rejection
无硬编码 数字和字符串提取为常量 if (retries > 3) 而非 MAX_RETRIES
无 mutation 使用 spread、map、filter 而非修改 array.push() 而非 [...array, item]

七、完整验证循环示例

用第 13 课写的 slugify 代码走一遍完整流程:

# Step 1: TEST
node --test slugify.test.js
# ✓ 7 tests passed → PASS

# Step 2: LINT
npx eslint slugify.js slugify.test.js
# No errors → PASS

# Step 3: TYPECHECK(如果是 TypeScript 项目)
# npx tsc --noEmit
# 纯 JavaScript 项目可跳过此步

# Step 4: SECURITY
# 检查无硬编码密钥
grep -r "api_key\|secret\|password" slugify.js
# No matches → PASS

# 检查依赖漏洞
npm audit
# 0 vulnerabilities → PASS

# 全部 PASS → 可提交
git add slugify.js slugify.test.js
git commit -m "feat: add slugify utility with full test coverage

Implements URL-friendly text conversion with:
- Space to hyphen conversion
- Special character removal
- Consecutive hyphen collapsing
- Input type validation

Coverage: 100% (8/8 tests passing)"

八、本课练习

练习 1:完整验证循环(25 分钟)

用第 13 课写的 slugify 代码(或第 13 课练习中的 truncate 代码),完整执行一遍验证循环:

  1. 运行测试,确认全部 PASS
  2. 运行 Lint,修复所有问题
  3. 运行类型检查(如适用)
  4. 执行安全检查清单(逐项确认)
  5. 用 Conventional Commits 格式提交

记录每一步的输出和修复过程。

练习 2:故意失败(15 分钟)

slugify.js 中故意引入以下问题,然后尝试通过验证循环:

  1. 改变一个正则让测试失败
  2. 添加一个未使用的变量让 Lint 报错
  3. 硬编码一个假的 API Key 让安全检查报警

观察验证循环如何捕获每种问题。

练习 3:代码质量清单审查(10 分钟)

对照代码质量清单的 7 项,审查你在第 13 课写的代码:

  • 函数是否 <50 行?
  • 有没有硬编码值?
  • 有没有 mutation?
  • 命名是否清晰?

列出所有不符合项并修复。

练习 4(选做):审查一个开源项目

找一个你熟悉的开源项目的某个 PR,用 ECC 的代码质量清单和安全检查清单审查它。记录你发现了什么问题。


九、本课小结

你应该记住的 内容
验证循环四步 TEST → LINT → TYPECHECK → SECURITY → 可提交
失败处理 任何一步失败 → 修复 → 从头重跑
安全检查清单 8 项强制检查(密钥、输入验证、注入、XSS、CSRF、认证、限流、错误消息)
Conventional Commits <type>: <description> — feat/fix/refactor/docs/test/chore/perf/ci
代码质量清单 7 项人工确认(可读性、函数大小、文件大小、嵌套、错误处理、硬编码、mutation)
安全响应协议 停止 → security-reviewer → 修复 → 轮换密钥 → 全局排查

十、下节预告

第 15 课:会话管理 — 上下文、模型与持久化

代码写好了、提交了,但还有一个隐藏的问题:你的 AI 助手的上下文窗口是有限的。下节课我们将学习如何管理上下文窗口、选择合适的模型、在会话之间持久化状态。这些技能决定了你能否在大型项目中高效使用 ECC。

预习建议:阅读 rules/common/performance.md,特别是 Model Selection Strategy 和 Context Window Management 两节。

第 13 课:TDD 全流程 — RED-GREEN-IMPROVE

所属阶段:第三阶段「工作流实战」(第 12-20 课) 前置条件:第 12 课(调用链追踪) 本课收获:完整体验一次 TDD 循环,理解每个阶段的目的


一、本课概述

TDD(Test-Driven Development)不是 ECC 发明的,但 ECC 把它从"建议"变成了"强制"。在 ECC 的世界里,没有测试的代码 = 不存在的代码。

本课回答三个问题:

  1. TDD 在整个开发流程中处于什么位置? — 四阶段开发流
  2. TDD 的每个阶段到底做什么? — RED-GREEN-IMPROVE-VERIFY 详解
  3. 如何亲手执行一次完整的 TDD 循环? — 从零写一个 slugify 函数

学完本课,你将能独立完成一次严格的 TDD 循环,包括测试先行、最小实现、重构优化和覆盖率验证。


二、四阶段开发流

在 ECC 的 development-workflow.md 中定义了完整的功能开发流程。TDD 不是孤立的,它是四阶段开发流中的第二步:

阶段 0:Research & Reuse(研究与复用)
    │ GitHub 搜索、库文档、包注册表
    │ "找到能解决 80%+ 问题的现有实现"
    ▼
阶段 1:Plan(规划)                    ← planner Agent
    │ 需求分析 → 架构设计 → 步骤拆解
    │ "必须等用户确认后才动手"
    ▼
阶段 2:TDD(测试驱动开发)              ← tdd-guide Agent
    │ RED → GREEN → IMPROVE → VERIFY
    │ "测试先行,80%+ 覆盖率"
    ▼
阶段 3:Review(代码审查)               ← code-reviewer Agent
    │ 安全检查 → 质量检查 → 分级报告
    │ "CRITICAL 和 HIGH 必须修"
    ▼
阶段 4Commit & Push(提交)
    │ Conventional Commits 格式
    │ CI/CD 通过后才能合并
    ▼
阶段 5:Pre-Review Checks(合并前检查)
    │ 自动检查通过 → 解决冲突 → 请求 Review

关键洞察:TDD 之前必须有 Plan,TDD 之后必须有 Review。跳过任何一步都会导致后续步骤的质量下降。


三、TDD 严格循环详解

3.1 四个阶段

ECC 的 TDD 循环比传统的 RED-GREEN-REFACTOR 多了一步 VERIFY:

┌─────────────────────────────────────────────────┐
│                TDD 严格循环                       │
│                                                  │
│  ┌──────┐    ┌──────┐    ┌──────────┐   ┌──────┐│
│  │ RED  │ →  │GREEN │ →  │ IMPROVE  │ → │VERIFY││
│  │写测试│    │最小实现│    │重构优化   │   │覆盖率 ││
│  │必须失败│   │刚好通过│    │测试仍绿  │   │80%+  ││
│  └──────┘    └──────┘    └──────────┘   └──────┘│
│      ▲                                     │     │
│      └─────── 下一个功能点 ◀───────────────┘     │
└─────────────────────────────────────────────────┘

3.2 RED 阶段 — 写测试,必须失败

做什么:写一个描述期望行为的测试,然后运行它,确认它失败

为什么测试必须先失败?

这是 TDD 中最反直觉但最重要的要求。理由有三:

  1. 验证测试本身有效 — 如果测试在实现之前就通过了,说明测试写错了(可能断言太宽松,或者测试的功能已经存在)
  2. 确认因果关系 — 只有先看到"失败",再看到"通过",你才能确信是你写的代码让测试通过的,而不是其他原因
  3. 明确需求 — 写不出失败测试 = 你还没想清楚要做什么(回到 Plan 阶段)
RED 阶段 = 需求定义阶段

测试写得出来 → 需求明确 → 继续
测试写不出来 → 需求不清 → 回到 Plan

3.3 GREEN 阶段 — 最小实现,刚好通过

做什么:写最少的代码让测试通过。不多写一行。

为什么是"最少"的代码?

  • 避免过度工程化 — 只实现测试要求的行为,不猜测未来需求(YAGNI)
  • 保持每步可验证 — 小步前进,每步都有测试保驾护航
  • 为重构留空间 — GREEN 阶段的代码可以很丑,重构在 IMPROVE 阶段做

常见错误:GREEN 阶段就开始优化代码结构。这是错误的 — 先让测试通过,再优化。

3.4 IMPROVE 阶段 — 重构优化,测试仍绿

做什么:在测试通过的保护下,改善代码质量。每次修改后运行测试,确保测试仍然通过。

可以做的事:

  • 提取常量(消除 Magic Numbers)
  • 重命名变量(提高可读性)
  • 提取辅助函数(降低复杂度)
  • 消除重复代码(DRY)
  • 改用不可变模式(Immutability)

铁律:重构期间测试必须保持绿色。如果测试变红了,说明重构改变了行为 — 回退并重来。

3.5 VERIFY 阶段 — 覆盖率检查

做什么:运行覆盖率工具,确保达到 80% 以上的覆盖率。

# Node.js 项目
npm test -- --coverage

# 覆盖率要求
# 常规代码:80%+
# 关键代码(金融计算、认证逻辑、安全相关):100%

四、AAA 模式(Arrange-Act-Assert)

每个测试用例都应该遵循 AAA 模式。这是 ECC testing.md 中明确推荐的结构:

test('returns empty array when no markets match query', () => {
  // Arrange — 准备测试数据和环境
  const markets = [
    { name: 'BTC-USD', volume: 1000 },
    { name: 'ETH-USD', volume: 500 },
  ];
  const query = 'DOGE';

  // Act — 执行被测行为
  const result = filterMarkets(markets, query);

  // Assert — 验证结果
  expect(result).toEqual([]);
});

4.1 三个阶段的职责

阶段 职责 常见错误
Arrange 准备输入数据、Mock 依赖、设置初始状态 使用过于复杂的 Mock
Act 调用被测函数/方法,仅此一步 在 Act 中做多个操作
Assert 验证输出、副作用、异常 断言太宽松(如只检查非空)

4.2 测试命名规范

ECC 推荐使用描述行为的测试名称:

// GOOD — 描述被测行为和条件
test('returns empty array when no markets match query', () => {});
test('throws error when API key is missing', () => {});
test('falls back to substring search when Redis is unavailable', () => {});

// BAD — 描述实现细节
test('test filterMarkets function', () => {});
test('test error', () => {});
test('test Redis fallback', () => {});

命名公式:{行为} when {条件}


五、实战:slugify 函数的 TDD 循环

下面我们完整走一遍 TDD 循环,实现一个 slugify 函数(将文本转为 URL 友好的格式)。

5.1 RED — 写测试,确认失败

// slugify.test.js
const { slugify } = require('./slugify');

describe('slugify', () => {
  test('converts spaces to hyphens', () => {
    // Arrange
    const input = 'hello world';

    // Act
    const result = slugify(input);

    // Assert
    expect(result).toBe('hello-world');
  });

  test('converts to lowercase', () => {
    expect(slugify('Hello World')).toBe('hello-world');
  });

  test('removes special characters', () => {
    expect(slugify('hello! @world#')).toBe('hello-world');
  });

  test('collapses consecutive hyphens', () => {
    expect(slugify('hello   world')).toBe('hello-world');
  });

  test('trims leading and trailing hyphens', () => {
    expect(slugify(' hello world ')).toBe('hello-world');
  });

  test('returns empty string for empty input', () => {
    expect(slugify('')).toBe('');
  });
});

运行测试:

node --test slugify.test.js

# 预期输出:6 个测试全部 FAIL
# Error: Cannot find module './slugify'

确认失败。测试本身是有效的 — 它在期望一个尚不存在的模块。

5.2 GREEN — 最小实现

// slugify.js
function slugify(text) {
  return text
    .toLowerCase()
    .replace(/[^a-z0-9\s-]/g, '')
    .replace(/\s+/g, '-')
    .replace(/-+/g, '-')
    .replace(/^-|-$/g, '');
}

module.exports = { slugify };

运行测试:

node --test slugify.test.js

# 预期输出:6 个测试全部 PASS

确认通过。注意这里的实现是"刚好够用"的 — 没有处理 Unicode、没有做参数校验、没有导出类型定义。

5.3 IMPROVE — 重构优化

// slugify.js — 重构后
const PATTERNS = {
  NON_ALPHANUMERIC: /[^a-z0-9\s-]/g,
  WHITESPACE: /\s+/g,
  CONSECUTIVE_HYPHENS: /-+/g,
  LEADING_TRAILING_HYPHENS: /^-|-$/g,
};

function slugify(text) {
  if (typeof text !== 'string') {
    throw new TypeError('slugify expects a string argument');
  }

  return text
    .toLowerCase()
    .replace(PATTERNS.NON_ALPHANUMERIC, '')
    .replace(PATTERNS.WHITESPACE, '-')
    .replace(PATTERNS.CONSECUTIVE_HYPHENS, '-')
    .replace(PATTERNS.LEADING_TRAILING_HYPHENS, '');
}

module.exports = { slugify };

改进了什么:

  • 提取正则为命名常量(消除 Magic Patterns)
  • 添加参数类型检查(输入验证)
  • 保持不可变 — 每个 replace 返回新字符串,不修改原始值

运行测试确认仍然通过:

node --test slugify.test.js

# 预期输出:6 个测试仍然全部 PASS

此时可以补充一个类型检查的测试:

test('throws TypeError for non-string input', () => {
  expect(() => slugify(123)).toThrow(TypeError);
  expect(() => slugify(null)).toThrow(TypeError);
});

5.4 VERIFY — 覆盖率检查

npx c8 node --test slugify.test.js

# 预期输出:
# File        | % Stmts | % Branch | % Funcs | % Lines
# ------------|---------|----------|---------|--------
# slugify.js  |   100   |   100    |   100   |   100
#
# Coverage: 100% PASS (Target: 80%)

TDD 循环完成。


六、development-workflow.md 的完整流程

把 TDD 循环放回完整开发流程的上下文中:

0. Research & Reuse
   │ ├─ GitHub 搜索:gh search repos / gh search code
   │ ├─ 库文档:Context7 / 官方文档
   │ ├─ Exa:前两步不够时的补充
   │ └─ 包注册表:npm / PyPI / crates.io
   ▼
1. Plan(planner Agent, Opus 模型)
   │ ├─ 需求分析和重述
   │ ├─ 架构变更清单
   │ ├─ 分阶段实施步骤
   │ └─ 等待用户确认
   ▼
2. TDD(tdd-guide Agent, Sonnet 模型)
   │ ├─ RED:写失败测试
   │ ├─ GREEN:最小实现
   │ ├─ IMPROVE:重构
   │ └─ VERIFY:80%+ 覆盖率
   ▼
3. Review(code-reviewer Agent, Sonnet 模型)
   │ ├─ CRITICAL / HIGH / MEDIUM / LOW 分级
   │ └─ 修复 CRITICAL 和 HIGH
   ▼
4. Commit
   │ ├─ Conventional Commits 格式
   │ └─ feat / fix / refactor / docs / test / chore / perf / ci
   ▼
5. Pre-Review Checks
   │ ├─ CI/CD 通过
   │ ├─ 冲突解决
   │ └─ 分支同步

七、TDD 反模式

ECC 的 tdd-guide.md 明确列出了必须避免的反模式:

反模式 为什么有害
先写实现再补测试 测试变成了"确认现有行为"而非"定义期望行为"
跳过 RED 阶段 无法确认测试本身是有效的
GREEN 阶段写太多代码 引入未经测试的行为,违反 YAGNI
测试实现细节而非行为 重构时测试就碎了,维护成本极高
测试之间共享状态 一个测试的失败会连锁影响其他测试
Mock 所有东西 测试只验证了 Mock 的行为,不验证真实行为
断言太少 测试通过但实际没验证任何有意义的事情

八、本课练习

练习 1:完整 TDD 循环(30 分钟)

严格按照 RED → GREEN → IMPROVE → VERIFY 的顺序,为以下函数完成一次 TDD 循环:

函数需求truncate(text, maxLength) — 截断文本到指定长度,超过时添加 "..."

测试用例提示:

  • 短于 maxLength 时原样返回
  • 等于 maxLength 时原样返回
  • 长于 maxLength 时截断并加 "..."
  • maxLength 小于 3 时的边界处理
  • 空字符串输入
  • 非字符串输入

严格要求

  1. 先写所有测试,运行确认全部 FAIL
  2. 写最少代码让测试通过
  3. 重构后再跑一遍测试
  4. 检查覆盖率

练习 2:识别反模式(10 分钟)

以下代码有什么 TDD 反模式?列出所有问题:

test('test the processOrder function', () => {
  const order = processOrder({ items: [{ id: 1, price: 10 }] });
  expect(order).toBeTruthy();
});

练习 3(选做):阅读 tdd-guide Agent

打开 agents/tdd-guide.md,回答:

  • 它的 model 是什么?为什么不用 Opus?
  • 它必须测试哪 8 种边界情况?
  • 它的质量检查清单有几项?

九、本课小结

你应该记住的 内容
四阶段开发流 Research → Plan → TDD → Review → Commit
TDD 四步 RED(写失败测试)→ GREEN(最小实现)→ IMPROVE(重构)→ VERIFY(80%+)
RED 阶段意义 验证测试有效、确认因果、明确需求
AAA 模式 Arrange(准备)→ Act(执行)→ Assert(断言)
测试命名 {行为} when {条件}
覆盖率要求 常规 80%+,关键代码 100%

十、下节预告

第 14 课:验证循环 — 从代码到可提交

TDD 只是验证的第一步。下节课我们将学习完整的验证循环:测试通过 → Lint 通过 → 类型检查通过 → 安全检查通过 → 才能提交。任何一步失败都要从头重跑。你会用第 13 课写的代码完整走一遍这个流程。

预习建议:阅读 rules/common/coding-style.md 的 Code Quality Checklist 和 rules/common/security.md 的 Mandatory Security Checks。

第 11 课:Scripts — Hook 的底层实现

所属阶段:第二阶段「组件精讲」(第 4-14 课) 前置条件:第 10 课 本课收获:能编写符合规范的 Hook 脚本并编写测试


一、本课概述

上节课我们学习了 Hook 的事件类型和配置格式。本课深入实现层scripts/ 目录。这里存放着所有 Hook 的实际代码。

本课回答三个问题:

  1. scripts 目录怎么组织? — 三个子目录各司其职
  2. Hook 脚本怎么写? — 标准模式、run-with-flags.js 包装器
  3. 怎么测试? — 测试规范和实战

二、目录结构

2.1 整体布局

scripts/
├── lib/                  # 共享库(工具函数)
│   ├── utils.js          # 跨平台工具函数
│   ├── package-manager.js # 包管理器检测
│   ├── hook-flags.js     # Hook 启用/禁用控制
│   ├── session-manager.js # 会话管理
│   ├── resolve-ecc-root.js # 解析 ECC 安装根目录
│   └── ...               # 其他共享模块
│
├── hooks/                # Hook 实现脚本
│   ├── run-with-flags.js # 核心包装器
│   ├── session-start-bootstrap.js
│   ├── pre-bash-commit-quality.js
│   ├── post-edit-console-warn.js
│   ├── stop-format-typecheck.js
│   ├── desktop-notify.js
│   └── ...               # 其他 Hook 脚本(30+ 个)
│
├── ci/                   # CI/CD 验证工具
│   ├── validate-agents.js
│   ├── validate-skills.js
│   ├── validate-hooks.js
│   └── ...               # 其他验证脚本
│
├── ecc.js                # CLI 入口
└── doctor.js             # 环境诊断工具

2.2 三个子目录的职责

目录 职责 被谁调用
lib/ 提供共享的工具函数 hooks/ 和 ci/ 中的脚本
hooks/ Hook 事件的具体实现 hooks.json 中的 command 字段
ci/ CI 流水线中的验证脚本 GitHub Actions

依赖关系

ci/ scripts
    └── require → lib/ (共享函数)

hooks/ scripts
    └── require → lib/ (共享函数)

lib/ 内部
    └── 模块之间也有 require 关系

三、代码约定

3.1 CommonJS Only

ECC 的所有脚本使用 CommonJS 模块系统,不使用 ESM

// 正确:CommonJS
const fs = require('fs');
const path = require('path');
const { getClaudeDir } = require('../lib/utils');

module.exports = { myFunction };

// 错误:不要用 ESM
import fs from 'fs';           // ✗
export default myFunction;      // ✗

原因:Node.js 18+ 虽然支持 ESM,但 CommonJS 在脚本工具中更简单直接,不需要处理 .mjs 扩展名、package.jsontype 字段等复杂性。

3.2 const 优先,禁止 var

// 好
const MAX_STDIN = 1024 * 1024;
const result = processInput(data);
let counter = 0;  // 确实需要重新赋值时用 let

// 差
var MAX_STDIN = 1024 * 1024;  // ✗ 永远不要用 var

3.3 Hook 脚本 <200 行

如果一个 Hook 脚本超过 200 行,说明它做了太多事情。正确做法:

# 差:300 行的 commit-quality.js

# 好:拆分
scripts/hooks/pre-bash-commit-quality.js  (80 行,入口)
scripts/lib/commit-validator.js           (120 行,核心逻辑)
scripts/lib/secret-detector.js            (60 行,密钥检测)

规则:Hook 脚本负责"胶水逻辑"(读 stdin、调用库函数、输出结果),核心逻辑提取到 lib/


四、Hook 脚本标准模式

4.1 通过 run-with-flags.js 运行的模式

大多数 ECC Hook 不直接被 hooks.json 调用,而是通过 run-with-flags.js 包装器运行。

hooks.json 中的调用方式

{
  "command": "node scripts/hooks/run-with-flags.js \"post:edit:console-warn\" \"scripts/hooks/post-edit-console-warn.js\" \"standard,strict\""
}

三个参数

参数 示例 说明
hookId post:edit:console-warn Hook 的唯一标识
scriptPath scripts/hooks/post-edit-console-warn.js 实际脚本的相对路径
profiles standard,strict 在哪些 Profile 下启用

4.2 run-with-flags.js 的作用

hooks.json 调用 run-with-flags.js
    │
    ├── 1. 检查 ECC_HOOK_PROFILE 环境变量
    │      当前 Profile 是否在允许列表中?
    │      不在 → exit 0(跳过)
    │
    ├── 2. 检查 ECC_DISABLED_HOOKS 环境变量
    │      当前 hookId 是否被禁用?
    │      是 → exit 0(跳过)
    │
    ├── 3. 读取 stdin(工具调用的 JSON 数据)
    │
    ├── 4. 加载实际脚本
    │      require(scriptPath)
    │
    ├── 5. 调用 module.exports.run(rawInput)
    │      或者 spawn 子进程执行
    │
    └── 6. 转发 exit code
           脚本的 exit code → run-with-flags.js 的 exit code

关键价值

  1. 统一管理启用/禁用 — 所有 Hook 的 Profile 检查在一处完成
  2. 支持环境变量控制 — 不改 hooks.json 就能调整 Hook 行为
  3. 统一 stdin 解析 — 不需要每个脚本自己解析 JSON

4.3 Hook 脚本的标准写法

'use strict';

function run(rawInput) {
  let input;
  try {
    input = JSON.parse(rawInput);
  } catch (err) {
    process.stderr.write('[HookName] Failed to parse input\n');
    process.exit(0);  // 解析失败不阻塞
  }

  const toolInput = input.tool_input || {};
  const filePath = toolInput.file_path || '';

  if (!filePath.endsWith('.js')) {
    process.exit(0);  // 不需要处理
  }

  try {
    doWork(filePath);
    process.stderr.write(`[HookName] Processed: ${filePath}\n`);
  } catch (err) {
    process.stderr.write(`[HookName] Error: ${err.message}\n`);
  }

  process.exit(0);
}

module.exports = { run };

4.4 关键规则总结

规则 原因
'use strict' 启用严格模式,捕获更多错误
JSON 解析失败 → exit 0 不因输入问题阻塞工具执行
stderr 带 [HookName] 前缀 方便日志排查
所有路径用 exit 0 兜底 防止意外拦截
提取 module.exports.run 让 run-with-flags.js 能加载和调用

五、包管理器检测优先级链

5.1 检测流程

scripts/lib/package-manager.js 实现了一个精心设计的优先级链来检测项目使用的包管理器:

优先级从高到低:

1. 环境变量 CLAUDE_PACKAGE_MANAGER
   │ 用户显式指定,最高优先级
   │
2. 项目配置文件 (.claude/config.json 中的 packageManager)
   │ 项目级别的配置
   │
3. package.json 中的 packageManager 字段
   │ Node.js 官方的 corepack 配置
   │
4. Lock 文件检测
   │ pnpm-lock.yaml → pnpm
   │ bun.lockb → bun
   │ yarn.lock → yarn
   │ package-lock.json → npm
   │
5. 全局配置 (~/.claude/config.json)
   │ 用户全局偏好
   │
6. 默认值:npm

5.2 支持的包管理器

const PACKAGE_MANAGERS = {
  npm:  { lockFile: 'package-lock.json', execCmd: 'npx',      ... },
  pnpm: { lockFile: 'pnpm-lock.yaml',   execCmd: 'pnpm dlx',  ... },
  yarn: { lockFile: 'yarn.lock',         execCmd: 'yarn dlx',  ... },
  bun:  { lockFile: 'bun.lockb',         execCmd: 'bunx',      ... }
};

5.3 Lock 文件检测顺序

Lock 文件的检测顺序是 pnpm → bun → yarn → npm,不是字母顺序。

原因:如果一个项目同时存在多个 lock 文件(这种情况在迁移过程中很常见),应该优先选择更现代的包管理器。


六、共享库 lib/ 详解

6.1 utils.js — 核心工具函数

scripts/lib/utils.js 提供跨平台的工具函数:

函数 作用
getHomeDir() 获取用户主目录(兼容 Windows/macOS/Linux)
getClaudeDir() 获取 ~/.claude 目录路径
getSessionsDir() 获取会话数据目录
readFile(path) 安全的文件读取(不存在返回 null)
writeFile(path, content) 安全的文件写入(自动创建目录)
commandExists(cmd) 检查命令是否存在

跨平台的关键:HOME(macOS/Linux)和 USERPROFILE(Windows)做了统一处理,优先读环境变量,兜底用 os.homedir()

6.2 hook-flags.js — Hook 启用控制

实现了第 10 课讲的 Profile 系统。核心函数 isHookEnabled(hookId, options) 检查两件事:是否被 ECC_DISABLED_HOOKS 显式禁用,以及当前 Profile 是否匹配。

6.3 resolve-ecc-root.js — 解析安装路径

ECC 可能安装在多个位置,此模块按优先级搜索:CLAUDE_PLUGIN_ROOT 环境变量 → ~/.claude/~/.claude/plugins/ecc/ → 市场安装路径 → 缓存安装路径。


七、测试规范

7.1 测试目录结构

测试目录镜像 scripts 目录结构:

tests/
├── run-all.js            # 测试运行器入口
├── lib/
│   ├── utils.test.js     # 对应 scripts/lib/utils.js
│   └── package-manager.test.js  # 对应 scripts/lib/package-manager.js
└── hooks/
    └── hooks.test.js     # Hook 集成测试

7.2 运行测试

# 运行所有测试
node tests/run-all.js

# 运行单个测试文件
node tests/lib/utils.test.js
node tests/lib/package-manager.test.js
node tests/hooks/hooks.test.js

7.3 测试编写规范

ECC 使用 Node.js 内置的 assert 模块,不依赖外部测试框架。每个测试用 try/catch 包裹,成功打印 PASS,失败打印 FAIL 并设置 process.exitCode = 1

const assert = require('assert');
const { getHomeDir } = require('../../scripts/lib/utils');

try {
  const home = getHomeDir();
  assert.ok(typeof home === 'string', 'getHomeDir returns string');
  assert.ok(home.length > 0, 'getHomeDir returns non-empty string');
  console.log('  PASS: getHomeDir');
} catch (err) {
  console.error('  FAIL: getHomeDir -', err.message);
  process.exitCode = 1;
}

7.4 新脚本必须有测试

这是 ECC 的硬性规则:

新增文件位置 测试要求
scripts/lib/xxx.js 必须tests/lib/xxx.test.js 添加测试
scripts/hooks/xxx.js 必须tests/hooks/ 添加至少一个集成测试
scripts/ci/xxx.js 建议有测试,但不强制(CI 脚本本身就是验证工具)

八、本课练习

练习 1:运行测试(5 分钟)

在项目根目录运行测试,确认所有测试通过:

node tests/run-all.js

回答问题:

  • 总共有多少个测试?
  • 有没有失败的测试?
  • 测试输出的格式是什么样的?

练习 2:阅读 run-with-flags.js(15 分钟)

打开 scripts/hooks/run-with-flags.js,回答:它接收几个命令行参数?怎么判断 Hook 是否应该运行?stdin 读取失败时怎么处理?

练习 3:为 utils.js 编写额外测试(20 分钟)

这是本课最重要的练习。

打开 tests/lib/utils.test.js,为 utils.js 中的一个函数编写额外测试用例。

建议测试的边界情况:

// 例如为 getHomeDir 测试边界情况:

// 1. 当 HOME 环境变量为空字符串时
const originalHome = process.env.HOME;
process.env.HOME = '';
const result1 = getHomeDir();
assert.ok(result1.length > 0, 'getHomeDir handles empty HOME');
process.env.HOME = originalHome;

// 2. 当 HOME 环境变量包含空格时
process.env.HOME = '/Users/my user';
const result2 = getHomeDir();
assert.ok(result2.includes('my user'), 'getHomeDir preserves spaces');
process.env.HOME = originalHome;

运行测试验证:

node tests/lib/utils.test.js

练习 4(选做):追踪 Hook 完整链路

选择 pre:bash:git-push-reminder,从 hooks.json 配置 → run-with-flags.js → 实际脚本,画出完整调用链。


九、本课小结

你应该记住的 内容
目录结构 lib/(共享库)、hooks/(Hook 实现)、ci/(CI 验证)
代码约定 CommonJS only、const 优先、Hook 脚本 <200 行
Hook 标准模式 module.exports.run = function(rawInput) {...}
run-with-flags.js 统一管理 Profile 检查、禁用检查、stdin 解析
包管理器检测 环境变量 → 项目配置 → package.json → lock 文件 → 全局配置 → npm
测试规范 tests/ 镜像 scripts/ 结构,新脚本必须有测试

十、下节预告

第 12 课:Commands — 用户交互入口

下节课我们进入 Commands 组件。Commands 是用户与 ECC 交互的最直接方式 — 输入 /tdd 就启动 TDD 工作流,输入 /plan 就开始规划。你将了解 79 个命令的分类、命令与 Agent 的映射关系,以及如何创建自定义命令。

预习建议:在 Claude Code 中输入 / 看看有哪些可用命令。打开 commands/ 目录浏览几个命令文件的格式。

财务数仓 Claude AI Coding 应用实战|得物技术

一、引言:财务数仓为什么需要AI?

财务数仓的特殊性

在电商数仓体系中,财务域是复杂度最高、容错率最低的领域。不仅因为财务对于数据准确性的要求高,也因为财务是横向域,与几乎所有的域都有数据交叉,因此对业务 Sense 的要求很高。财务数仓工程师本质上在做三件事:

  • 业务翻译: 将交易、支付、资金、促销补贴、成本等数十个业务系统的数据,翻译成通用的财务语言;
  • 资产架构: 从 ODS 到 DWD、DWS、ADS 层层构建,确保财务 UE、财务管报等公司核心指标算得准、算得快;
  • 质量兜底: GMV 口径是否统一,退款是否扣减,分摊是否跨周期对齐,任何一个字段的偏差都可能导致错误的经营决策。

财务域的独特挑战在于:字段间存在严格的数学公式关系(正向-冲销=冲销之后),业务规则涉及跨周期分摊,对于质量的要求极高。如果单纯依靠人工兜底,要么容易出错,要么需要冗余大量人力做复核。尤其是在交付压力大的时候,质量问题就更容易被忽视。

痛点聚焦

从财务数仓的特殊性出发,我们可以总结财务数仓的痛点,大体可以分为如下几类:

基本上,在需求承接的每个环节,都可能因为"人"的问题,带来隐患。

AI 大模型能带来什么改变

为了有效解决"人"的问题,比如催得太急、看不过来、没看仔细、理解错误等问题,我们引入 AI 来做改变。核心思路是:大模型的介入不是替代数仓开发工程师,而是在「需求理解 → 代码编写 → 质量测试 → 文档沉淀」每个环节注入强推理能力。利用 AI 来代替人做大量的重复性工作,同时减少低级错误概率。

那么为什么 AI 能做到这一点?从技术发展的趋势看,有三个核心能力支撑了这一变革:

  • 超大上下文打破知识孤岛: 200k+ token 的上下文窗口,可以将表结构定义、词根字典、指标计算逻辑一次性注入模型的 “工作记忆”,实现基于全域元数据的推演,让大模型具有记忆;
  • 业务语义的自动抽象与对齐: 大模型能理解 “日活”“留存率”“归因窗口” 等业务术语,并映射为具体 SQL 实现,减少因需求理解偏差导致的返工;Claude 在编码领域显著优于其他模型,是因为它能 “懂” 业务逻辑,而不是简单的机械执行;
  • 突破人类极限的规范执行力: 人工在紧迫工期下规范遵守率通常明显下降,而大模型注入规范后,可稳定维持在高位。只要指令给得明确,大模型 “几乎” 不会出错。

参考:亚马逊 AWS 对于构建一个强大、具备自我纠错能力且能查询多种数据源的 Text-to-SQL 解决方案架构图。

二、应用场景概览:从「单点提效」到「全链路增强」

场景与提效预期

基于上述观点,在财务领域,大模型可以在哪些具体的环节落地呢?以下是根据笔者近期实践经验,列出的可落地场景及提效预期。

人机协作模式:数仓研发的「L3 时刻」

如果借用自动驾驶的分级标准,当前数仓大模型应用正处于从 L2(辅助驾驶)向 L3(有条件自动驾驶)过渡的阶段,即在明确的 Prompt 约束与规范文档支撑下,AI 能接管绝大部分标准化的执行动作。

在财务域的实践中,我们也是按照这套自动驾驶分级的方法,将日常工作拆解成了三级:

这种分工背后的逻辑是:规范执行是人类的短板、AI 的长板;业务判断是 AI 的短板、人类的长板。 人工在紧迫工期下对命名规范、分区约束、注释要求的遵守率通常明显下降,且容易因疲劳产生遗漏;而 AI 一旦"学会"了团队规范,输出的规范遵守度可稳定维持在较高水平。反过来,AI 无法替代的是那些需要理解业务上下文、权衡取舍、处理分歧的工作。

AI 对于数仓全链路研发的提效作用

学习 Andrej Karpathy 关于 ChatGPT 分享的内容时,最大的感受是:AI 最强的能力,是 "泛化"。 因此,如果我们可以把数仓研发的链路拆分清楚,那么 AI 必然能够对其中的每一个环节提效,最终带来研发效率的大幅度提升!

三、核心应用场景深度解析

AI OneData 标准化建模(财务核算数据项目)

背景:财务核算 OneData 为什么难搞?

因为:仅第一轮模型设计,就涉及百张以上的表、多个子域、十余个业务过程、数百个指标。如果考虑到后续的二次/三次迭代,工作量势必大到无法想象。在当前以交付为主的阶段,很难花费如此多的时间做基建。以某次核算项目为例,各层表数量分布如下:

同时,财务域的核心特征是来源多(全公司系统)、指标多(单表字段数众多),但以可累加指标为主。财务严格意义上没有原子指标,全是基于业务指标加工出来的派生指标,且一个财务指标往往有多种口径:业务口径、资金口径、财管报口径。并且,项目涉及多个子域(核算域、技术成本域、促销补贴域、商业化域、分析域),覆盖从「计费 → 核算 → 结算 → 财务分析」的端到端业务过程体系。如果要彻底理解核算 OneData 的构建,不仅要懂数仓,还要懂财务,还要熟悉公司财务系统,这个要求非常难做到!主要难点集中在四个方面:

口径溯源极其复杂: 大量逻辑在工程侧实现,绝大多数表缺失业务文档、技术文档、口径文档,口径逻辑需要基于代码猜测,存在错误可能性,溯源工作量巨大。

规范执行不一致: 财务域涉及表命名规范(DIM/DWD/DWS/ADM 各有格式要求)、时间周期规范(1d/7d/30d/wtd/mtd/ytd 等多种)、生命周期规范、刷新周期规范、标准字段英文命名原则({主体}{业务场景}{币种标识}{度量类型}{时间单位})。规范越细,人工遵守率越低。

跨域依赖复杂: 财务是横向域,与各业务域交叉。核算域依赖大量上游表,技术成本域需要从云服务、算法、产研人力、标注人力等多个来源接入数据。

文档输出繁琐: 每个 ADM 表都必须包含 OnePage 文档(OneData 方案最重要内容),加上口径文档、模型使用说明、下游 mapping 文档,文档间大量重复但需各写一遍。

所以,我们更需要通过 AI 的能力,来做一套新时代的建模方法论,以适应 “低投入、大设计” 的智能建模场景。

建模方法论:规范即 Prompt × 迭代收敛法 × 海量文件阅读

第一个方法论:规范沉淀是前提

AI 的输出质量完全取决于输入的规范文档质量。财务核算项目中,我们沉淀了完整的规范体系作为 Prompt 的核心输入,包括:

  • 模型设计规范:表命名、时间周期、生命周期、刷新周期;
  • 标准字段英文命名原则:{主体 /fin}{业务场景 / 费用类型}{币种标识}{度量类型}{时间单位};
  • 财务业务全链路设计理念:计费层 → 核算层 → 结算层 → 财务分析层;
  • 业务过程总线矩阵:多个业务过程与多个维度的交叉关系;
  • 数据质量监控规范:完整性、准确性、一致性、合规性、业务规则等多个大类。

第二个方法论:迭代是常态

不要期望 AI 一次给出完美结果。验证的关键是选择复杂字段进行抽查 —— 在财务场景中,重点验证涉及条件取值的字段(如分摊逻辑、冲销逻辑、多口径指标),对照 SQL 代码验证溯源路径。每次迭代的产物不只是修正后的输出,更重要的是规范文档的完善。因此,针对每次迭代的结果,快速识别要改动的点并修改,这一点就很重要。也就是说,AI 可以显著提升我们的迭代速度!

第三个方法论:海量文件阅读

因为超大的 Context,所以不仅可以把历史上已有的文档一次性灌入进去,也可以把原有设计链路的表和代码交给大模型理解,省去大量阅读和理解的时间。同时,能够帮我们精准地画出业务架构图,辅助数仓工程师理解业务、构建模型。例如财务数仓架构图,很多子模块的逻辑,都是大模型读取代码后输出思路,再由数仓团队整理形成的。

Prompt 和效果

将以上规范作为学习知识输入给模型,再把原始数据表给到模型,模型即可以产出建模建议。

Prompt 示例:

请读取以下规范文档:

  • 数仓规范资产细则(含词根字典、命名规范);
  • 离线数仓开发规范白皮书;
  • 团队 Cursor Rules;

分析目标表(输入对应的表名)的建表语句,按照数仓建模规范(ODS → DWD/DIM → DWS → ADM)的方式,输出重构后的建模建议。

第一次生成的效果展示了初步建模建议,在经过不断的调优和知识输入后,最终版本要丰富很多,形成了完整的财务核算数据 OneData 方案。

收益

经过一段时间的实施,第一版核算数据结构已经落地,效果如下:

  • 效率提升显著: 百张表的口径溯源、文档输出等标准化工作大幅压缩;
  • 规范遵守率大幅提升: 表命名、字段命名、时间周期等规范严格执行,遵守率较人工有明显改善;
  • 可复用性强: 规范文档、工具脚本、Prompt 模板、工作流程 SOP 均可跨子域复用(已在核算域、技术成本域验证);
  • 数据质量监控体系: 基于口径逻辑自动推荐 DQC 规则(完整性、准确性、一致性、合规性、业务规则等多大类)。

AI SQL Coding 实践(财务 UE 表迭代案例)

实践思路

以财务 UE 表某次迭代为代表的案例,主要成果有:

  • 代码结构优化,可读性大幅提升: 指标分段清晰、逻辑分层明确,维护成本明显降低;
  • 代码开发速度提升: 在规范与口径已对齐的前提下,从需求到可上线代码耗时缩短;
  • 性能优化: 整体基线提前完成,为下游留出更多缓冲时间。

那么,我们是如何实现这种成果的?主要靠两点,一是 PRD 快速阅读与理解,二是代码开发效率提升。

如何理解 SQL Coding 核心能力

PRD 阅读与理解方面,AI 能够帮我们实现:

快速将 PRD 中的目标、指标、维度、过滤条件提炼为结构化要点;对「大促期间」、「小仓卖家」、「冲销」等未精确定义的表述,自动生成待确认问题清单;输出「指标口径」「统计周期」「主键与粒度」等需确认条目。

代码开发效率提升方面,AI 能够帮我们实现:

基于词根、分层、命名规范与建表模板,生成符合数仓规范的 DDL 与 SELECT 语句;多维度聚合、归因逻辑、窗口函数、多层嵌套等复杂逻辑,由模型生成初版 SQL,人工校验微调;对存量长 SQL 进行分段、抽取公共逻辑、统一风格与注释。

实践中大模型显著提升点

财务 UE 表迭代需求使用 AI 开发后,具体效果如下:

指标结构分段、编码规范性、注释清晰度:

  • 新表:按数仓分层与命名规范生成 DDL 与 SQL,指标按业务域/统计口径分段组织,注释完整(字段含义、口径说明、KEY 标记等),既符合规范又便于阅读。
  • 旧表改造:在保留业务逻辑正确性的前提下,对历史「屎山」代码进行结构化改写——统一别名、补全注释、拆分过长子查询、显式写出分区过滤等,使后续维护与排查成本明显下降。
  • 代码展示对比:改动前 vs 改动后,可从「可读性、规范遵守度、注释覆盖」等维度做对比分析。

代码撰写速度大幅度提升:

  • AI Coding 的主要步骤:Step 1:整理需求 → 技术文档 将 BI 需求文档中的字段信息整理进技术文档,明确字段范围。
  • Step 2:大模型分析字段来源 提示大模型读取 DWD 源码,分析哪些字段已存在、哪些需要新增关联。
  • Step 3:大模型编写 ETL 代码 由大模型自动在 DWD → DWS → ADM 三层添加字段代码,输出改动代码集合。
  • Step 4:命名规范校准 引入指标字典和 Cursor Rules,让大模型按规范重命名字段(去掉不规范后缀)。
  • Step 5:测试 SQL 生成与跑数验证 大模型生成自测 SQL,逐步验证各层数据一致性,不通过时追问原因并溯源。

性能优化及自动调参:

  • 自动识别性能瓶颈:结合执行计划、大表扫描、数据倾斜等常见问题,由模型分析 SQL 与表结构,指出潜在慢点。
  • 优化建议生成:在分区裁剪、谓词下推、JOIN 顺序、中间结果物化等方面给出具体改写建议。
  • 参数调优方案:针对 Spark/ODPS 等引擎的资源配置、并行度、倾斜处理参数,给出可落地的调优建议,供运维或开发同学选用。

基线优化提升案例:

  • 原链路:多张表串行/并行产出,整体耗时较长。
  • 新链路:经模型辅助做表合并与逻辑下沉,收敛至更少的表,整体耗时明显缩短。
  • 优化效果:在保证口径一致的前提下,表数量与运行时间双降,基线提前完成,资源占用与调度依赖均得到简化。

AI 数据测试(财务 UE 表邮费迭代案例)

财务数据测试的特殊挑战

在数仓开发工作中,数据测试是保障数据质量的关键环节,但也是最复杂、最耗时的环节之一。特别是在财务类指标开发中,数据测试面临着多重挑战:

测试复杂度高,影响面广:

一个指标的改动往往不是孤立的,它会引发连锁反应,影响其他相关计算指标。在复杂的业务场景中,一个字段的修改可能需要同步验证数十个相关字段的正确性。这种复杂的依赖关系使得人工测试很难做到全面覆盖,容易出现遗漏

业务逻辑复杂,公式验证困难:

财务指标通常有明确的数学公式关系:正向 - 冲销 = 冲销之后:需要验证每个字段的正向值、冲销值、冲销之后值之间的计算关系;子项相加 = 汇总项:需要验证各个子项字段相加是否等于汇总字段;

财务的分摊逻辑涉及跨周期问题,难以验证:某些业务场景下,订单时间与收入确认时间不匹配,需要进行跨周期分摊,测试逻辑极其复杂。这些公式关系看似简单,但在实际测试中,需要考虑各种边界情况、精度问题、空值处理等,验证工作量巨大。

测试用例设计困难:

一个需求往往衍生出大量测试点,单纯凭借个人经验和能力,很难做到全面覆盖,容易出现测试盲区,包括:

  • 字段级别的计算逻辑验证;
  • 汇总关系的验证;
  • 冲销逻辑的验证;
  • 边界场景的验证;
  • 精度问题的验证;
  • 业务规则转化的验证。

业务语言到数据语言的转化困难:

业务人员描述的需求往往是自然语言,而数据测试需要将其转化为精确的数据验证逻辑。例如:"退小仓场景下,卖家邮费出资放在第一笔收入冲销,挂在最后一单";"邮费返利抵减技术服务费";"跨周期分摊,商业化订单时间与交易订单时间不匹配"。

AI 在数据测试中的应用实践

那么,我们如何通过 AI,来解决这些复杂问题呢?以某次财务 UE 表邮费迭代项目为例,我们深度应用 AI 进行数据测试,取得了显著效果。

项目背景:

该项目涉及邮费相关字段的全面重构,包括:

  • 迭代字段:修改多个邮费相关字段的计算逻辑;
  • 新增字段:新增大批量邮费细分字段;
  • 删除字段:废弃部分历史字段;
  • 逻辑变更:邮费返利抵减逻辑调整、冲销逻辑优化等。

AI 应用场景:

  1. 测试用例自动生成:向 AI 提出测试要求后,AI 能够自动生成完整的测试 SQL 和说明文档,包括:
  • 正向-冲销=冲销之后的验证逻辑;
  • 子项相加等于汇总项的验证逻辑;
  • 业务规则转化的验证逻辑;
  • 边界场景的验证逻辑。
  1. 规则理解层面的测试补充:AI 能够从规则理解层面补充测试案例,如抽样验证、精度验证等,减少因理解不一致带来的质量问题。特别是在复杂的跨周期分摊场景中,AI 能够识别出人工容易忽略的测试点。

  2. 复杂逻辑的逐步分析:针对复杂的业务逻辑,AI 能够逐步分析不符合预期的环节,帮助找到潜在的代码 Bug。例如在邮费冲销逻辑中,AI 能够分析退小仓场景下的多种分支情况,识别出逻辑漏洞。

  3. 上下游影响分析:AI 能够分析一个字段的改动对上下游的影响,帮助识别需要同步验证的相关字段,避免遗漏。

  4. 公式验证与精度问题诊断:AI 能够自动生成公式验证 SQL,并识别精度问题。在测试过程中,AI 能够区分真正的逻辑错误和可接受的精度误差,避免误报。

实际效果与收益

经过 AI 加持之后,效果和收益明显,包括:

开发效率提升:

测试 SQL 生成效率明显提升:从提出测试要求到生成完整测试 SQL,时间大幅缩短;测试用例覆盖度提升:AI 能够识别出人工容易忽略的测试点,测试覆盖更全面。

交付质量提升:

一次交付通过率显著提升:从规则理解层面补充测试案例,减少理解不一致带来的质量问题;针对复杂逻辑逐步分析,找到潜在代码 Bug;自动生成全面的测试用例,减少测试盲区。

问题发现能力提升:

AI 在测试过程中能够:发现人工难以发现的逻辑错误,识别精度问题并区分可接受的误差,分析复杂的业务规则转化问题,诊断上下游影响关系。

综合收益较高。通过 AI 辅助数据测试,整体交付质量大幅提升,主要体现在:测试覆盖更全面,减少遗漏,问题发现更及时,减少返工,测试效率更高,缩短测试周期,质量保障更可靠,提升交付信心。

AI 需求文档转换(财务 UE 表邮费复杂逻辑解读)

痛点

理解 PRD 和与业务产品反复核对口径,大约占数仓总体工作时间的较大比例。BI 需求文档往往复杂难懂,第一眼看过去看不懂。

实践案例:邮费 UE 迭代技术文档

以邮费 UE 迭代需求为例,BI 需求文档涉及大量字段口径调整、新增字段、废弃字段、冲销逻辑重写等复杂内容。例如通过飞书 MCP 让 Cursor 直接读取 BI 需求文档,大模型自动总结出两张表(DWS 层和 ADS 层)各自需要改什么。大模型输出的结论结构清晰,按表分类列出:

  • 字段含义/口径调整(哪些字段的逻辑需要改);
  • 数据来源与计算点(应收邮费、实收邮费的新口径);
  • 新增字段清单(应收拆分、冲销相关、实收拆分、成本、UE 等);
  • 废弃字段清单(相关历史字段);
  • 冲销逻辑重点(退小仓规则);
  • 两表关系与实现顺序(先改 DWS 再改 ADS)。

Prompt 实例:读取「邮费逻辑梳理」文档内容,分析其文字描述与财务 UE 表的代码,分析要改动的点,帮我生成对应改动代码和改动原因注释。

通过这个分析结果,能够很快地定位要改动的代码,然后一步步理解业务逻辑和具体如何改动。

效果

经过这个过程快速 get 到 PRD 缺失的内容、快速对齐,总体沟通时间有效缩减。虽然在总时间占比上看似不高,但节省的是工程师最头疼的碎片化沟通时间。

四、总结与展望

核心价值

当前市场上,部分头部大厂由于自身产品策略的原因,限制了内部使用最新的大模型和 IDE 工具,导致一线使用大模型的效率受到制约。而我们则能够更灵活地选择最适合的工具组合,在使用技巧和经验积累上具备优势。例如,我们有如下两个方面的优势:

能力层面:

  • 规范化规则遵守:注入规范后生成结果遵守度稳定维持在高位;
  • 业务抽象能力:快速理解 PRD 中的目标、指标与口径,识别模糊点;
  • 实际落地案例丰富:财务 UE 表迭代等项目已有可量化结果。

组织与场景层面:

  • 模型选择灵活,不绑定单一厂商,按任务类型选用最优模型;
  • 组织精简高效,从确定方向到试点上线路径清晰,试错迭代周期短;
  • 离线数仓分层与规范稳定,模型易学易用、效果可预期;
  • 离线任务可重跑、可回溯,模型产出便于充分校验后再上线。

未来展望

使用大模型的能力不仅仅局限在财务、局限在个人,也要向整个团队推广,包括:优先选择 1-2 个痛点明确、规范相对清晰的场景做试点;将有效的 Prompt 设计、上下文组织方式、测试用例模板等经验在团队内分享,形成可复用知识库;从「人做」为主转向「人定规则与口径、模型执行环节」的协作模式,让大模型成为数仓同学的日常助手。未来已来。

往期回顾

1.日志诊断 Skill:用 AI + MCP 一键解决BUG|得物技术

2.Redis 自动化运维最佳实践|得物技术 

3.Claude在得物App数仓的深度集成与效能演进

4.Claude Code + OpenSpec 正在加速 AICoding 落地:从模型博弈到工程化的范式转移|得物技术

5.大禹平台:流批一体离线Dump平台的设计与应用|得物技术

文 /丹克

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

使用 Ollama 在本地运行 AI Agent — 不需要 API Key

背景

如果你的目标是学习 Agent 概念、测试架构设计、做技术调研,根本不需要付费 API。Ollama 可以让你在本地电脑上运行开源大模型,并且提供 OpenAI 兼容的 API 接口,主流 Agent 框架都能直接对接。

本文介绍如何用 Ollama 搭建一个完全本地化的 AI Agent 开发环境。无需 API Key,无需联网,无需任何费用。

三种方案怎么选

方案 适用场景 成本 前置条件
Ollama(本文) 学习、调研、架构实验、离线开发 免费 本地机器 8GB+ 内存
Claude Agent SDK 需要 Claude 级别智能的内部原型验证 共享订阅额度 Claude Code CLI + Enterprise 登录
LLM Gateway API 生产环境、对外服务 按 token 计费 审批 API Key

简单原则:先用 Ollama 验证想法、理解 Agent 运行机制。需要 Claude 级别推理能力时切换到 Agent SDK。上生产时申请 LLM Gateway API。

Ollama 是什么

Ollama 是一个开源的本地大模型运行工具,封装了 llama.cpp,提供简单的 CLI 和内置的 OpenAI 兼容 API 服务(http://localhost:11434)。核心特点:

  • 支持 Llama、Qwen、Mistral、DeepSeek、Gemma、Phi 等主流模型系列
  • GPU 加速:NVIDIA (CUDA)、Apple Silicon (Metal)、AMD (ROCm)
  • 内置 Tool Calling / Function Calling 支持(需要模型本身支持)
  • OpenAI 兼容 API — 改个 base_url 就能用,框架代码不用改
  • CLI 管理模型:ollama pullollama runollama list

快速开始

第一步:安装 Ollama

macOS / Linux:

curl -fsSL https://ollama.com/install.sh | sh

Windows:

winget install Ollama.Ollama

验证安装:

ollama --version

第二步:拉取模型

Agent 开发需要支持 Tool Calling 的模型,推荐:

# 推荐:Qwen3.5 9B — 最新一代,原生视觉 + Tool Calling + Thinking,256K 上下文
ollama pull qwen3.5:9b

# 轻量替代:Qwen3.5 4B — 8GB 内存机器也能跑
ollama pull qwen3.5:4b

# 更强推理能力(需要 24GB+ 内存):Qwen3.5 27B
ollama pull qwen3.5:27b

# MoE 选项 — 总参数 35B 但仅激活 3B,22GB 设备可运行
ollama pull qwen3.5:35b

硬件参考:qwen3.5:9b 约 6.6GB(大部分 16GB 机器轻松运行)。qwen3.5:4b 约 3.4GB(8GB 内存笔记本也行)。qwen3.5:27b 和 35b 需要 24GB+ 内存/显存。16GB 的 Apple Silicon Mac 可以流畅运行 9B 模型。

第三步:验证 API

Ollama 启动后自动提供 API 服务,测试一下:

curl http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3.5:9b",
    "messages": [
      {"role": "user", "content": "你好!"}
    ]
  }'

收到 JSON 回复就说明环境就绪。

使用 Ollama 构建 Agent

Ollama 的 OpenAI 兼容 API 意味着你可以无缝接入任何支持 OpenAI 格式的框架。以下是最常用的几种模式。

模式 A — 直接用 OpenAI SDK(最简单)

官方 OpenAI Python SDK 可以直接对接 Ollama,只需改 base_url

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:11434/v1",
    api_key="ollama",  # SDK 要求填写,但 Ollama 不校验
)

response = client.chat.completions.create(
    model="qwen3.5:9b",
    messages=[
        {"role": "system", "content": "你是一个有帮助的助手。"},
        {"role": "user", "content": "用3句话解释什么是 AI Agent。"},
    ],
)
print(response.choices[0].message.content)

模式 B — Tool Calling / Function Calling

Agent 开发的核心 — 让模型自主决定何时调用外部工具:

import json
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:11434/v1",
    api_key="ollama",
)

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的当前天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名称"}
                },
                "required": ["city"],
            },
        },
    }
]

response = client.chat.completions.create(
    model="qwen3.5:9b",
    messages=[{"role": "user", "content": "香港今天天气怎么样?"}],
    tools=tools,
)

msg = response.choices[0].message
if msg.tool_calls:
    for call in msg.tool_calls:
        print(f"模型要调用: {call.function.name}")
        print(f"参数: {call.function.arguments}")
else:
    print(msg.content)

模式 C — 完整 Agent 循环

一个最小化的 Agent 循环,展示完整流程:用户输入 → 模型推理 → 工具执行 → 模型综合回答:

import json
from openai import OpenAI

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

# 模拟工具实现
def get_weather(city: str) -&gt; str:
    return json.dumps({"city": city, "temp": "28°C", "condition": "晴"})

def search_docs(query: str) -&gt; str:
    return json.dumps({"results": [f"找到关于 '{query}' 的文档"]})

TOOL_MAP = {"get_weather": get_weather, "search_docs": search_docs}

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的当前天气",
            "parameters": {
                "type": "object",
                "properties": {"city": {"type": "string"}},
                "required": ["city"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "search_docs",
            "description": "按关键词搜索内部文档",
            "parameters": {
                "type": "object",
                "properties": {"query": {"type": "string"}},
                "required": ["query"],
            },
        },
    },
]

def agent_run(user_input: str):
    messages = [
        {"role": "system", "content": "你是一个有帮助的助手,需要时使用工具。"},
        {"role": "user", "content": user_input},
    ]

    # 第一步:模型初次调用
    response = client.chat.completions.create(
        model="qwen3.5:9b", messages=messages, tools=tools
    )
    msg = response.choices[0].message
    messages.append(msg)

    # 第二步:如果模型请求调用工具,执行它
    if msg.tool_calls:
        for call in msg.tool_calls:
            fn = TOOL_MAP.get(call.function.name)
            if fn:
                result = fn(**json.loads(call.function.arguments))
                messages.append({
                    "role": "tool",
                    "tool_call_id": call.id,
                    "content": result,
                })

        # 第三步:模型综合工具结果生成最终回答
        final = client.chat.completions.create(
            model="qwen3.5:9b", messages=messages, tools=tools
        )
        return final.choices[0].message.content

    return msg.content

# 试试
print(agent_run("香港今天天气怎么样?"))
print(agent_run("帮我搜索 Agent 架构相关文档"))

这就是所有 Agent 框架背后的基本模式。理解了这个循环,就可以逐步构建更复杂的 Agent。

可选:LiteLLM 统一网关

大部分本地开发场景 Ollama 就够了。但如果你需要:

  • 在多个模型之间路由(例如本地 Ollama + 云端备选)
  • 添加日志、成本追踪、限流
  • 用同一套代码测试多种模型后端

可以用 LiteLLM 作为 Ollama 前面的代理:

pip install litellm[proxy]

# 启动代理,指向本地 Ollama
litellm --model ollama/qwen3.5:9b --port 8000

然后应用指向 http://localhost:8000,代码还是标准 OpenAI SDK 格式,不需要任何改动。

当你的 Agent 代码不变但想通过配置而非代码来切换本地(Ollama)和云端(LLM Gateway)模型时,LiteLLM 很有用。

优势

  1. 零成本:不需要 API Key,不产生 token 费用,不消耗订阅额度。本机硬件跑多少都行。
  2. 完全隐私:数据完全在本地,不需要联网。用什么数据做实验都没有隐私顾虑。
  3. 理解原理:直接使用开源模型,能真正理解 Tool Calling、上下文管理、Agent 循环是怎么运作的,不被商业 API 的抽象挡住。
  4. 框架无关:Ollama 的 OpenAI 兼容 API 支持 LangChain、LlamaIndex、CrewAI、AutoGen、smolagents 等几乎所有主流框架。
  5. 快速迭代:没有速率限制,没有网络延迟。启动模型、测试、调整、重复。

限制与注意事项

  1. 模型能力差距:开源 8B–32B 模型在复杂推理、长上下文、精确 Tool Calling 上与 Claude Sonnet/Opus 有明显差距。工具参数生成和指令遵循的出错率更高。
  2. Tool Calling 可靠性:并非所有模型的 Tool Calling 都一样好。Qwen3.5、Llama 3.3、GLM-4 支持最好。小模型(<9B)容易出现工具调用幻觉或参数遗漏。Agent 任务建议 temperature 设为 0–0.2。
  3. 硬件要求:本地跑模型需要 CPU/GPU 和内存资源。8B 模型大部分机器没问题。32B+ 模型需要较强硬件(32GB+ 内存或 24GB+ 显存的独立显卡)。
  4. 不适合生产:本方案用于开发、学习和研究。生产服务请使用 LLM Gateway API,有 SLA、计费和监控保障。
  5. 无内置 MCP 支持:与 Claude Code 不同,Ollama 不原生集成 MCP Server。需要在 Agent 代码中自行实现 MCP 客户端逻辑,或使用支持 MCP 的框架。

从本地到生产

在本地用 Ollama 验证 Agent 设计后,切换到生产环境非常简单,因为 API 接口格式一致:

  1. 切换到 Claude Agent SDK 做内部测试 — 只需更换认证方式,代码结构不变。
  2. 切换到 LLM Gateway API 上生产 — 把 base_url 改成网关地址,api_key 填网关密钥。同样的 OpenAI SDK 代码,不同的端点。
# 本地开发(Ollama)
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

# 生产环境(LLM Gateway)
client = OpenAI(base_url="https://&lt;gateway-url&gt;/v1", api_key="your-gateway-key")

Agent 逻辑、工具定义、提示词完全不变。这就是基于 OpenAI 兼容 API 标准构建的核心好处。

Agent 开发推荐模型

模型 参数量 Tool Calling 适用场景 最低内存
qwen3.5:9b 9B 日常 Agent 开发、实验、代码任务 8GB
qwen3.5:4b 4B 一般 硬件受限时的轻量测试 8GB
qwen3.5:27b 27B 很好 复杂推理、接近生产级测试 24GB
qwen3.5:35b 35B (MoE, 3B 激活) 很好 较强能力 + 中等硬件要求 22GB
llama3.3:8b 8B 通用任务 8GB

运行 ollama list 查看已安装模型,ollama pull &lt;model&gt; 下载新模型。

参考资源

❌