Skip to content

Commit fa247a1

Browse files
authored
feat: build a mini claude code (#69)
* feat: build a mini claude code * fix: code review * docs: align mini-claude-code docs with implementation
1 parent b41730b commit fa247a1

37 files changed

Lines changed: 2418 additions & 0 deletions

‎.gitignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,8 @@ go.work.sum
2424
# env file
2525
.env
2626
.qodo
27+
28+
node_modules/
29+
dist/
30+
.DS_Store
31+
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Mini Claude Code
2+
3+
> Agent 开发实战课 —— 从零构建一个 Mini Claude Code(TypeScript)
4+
5+
## 收益
6+
7+
- 理解为什么 Agent 能"做事情",而 ChatBot 不能
8+
- 听得懂 ReAct 与 Agent 基本架构
9+
- 能跑通最小 TypeScript Agent
10+
- 带走可落地的工程经验
11+
12+
## 核心概念
13+
14+
**Agent = ReAct + Tools + UI**
15+
16+
| 概念 | 说明 |
17+
| ---------- | ------------------------------------------------ |
18+
| 上下文窗口 | 维护消息历史数组,作为模型的完整输入 |
19+
| 系统提示词 | 定义 Agent 身份、能力边界与输出规范 |
20+
| 工具调用 | 模型生成结构化指令,触发外部能力,结果回注上下文 |
21+
| ReAct 循环 | 观察 → 思考 → 行动,控制流从人转移到模型 |
22+
23+
## 教案目录
24+
25+
1. **Agent 是怎么工作的?** — ChatBot vs Agent 的本质差异
26+
2. **ReAct:让模型"边想边做"** — 循环架构与控制流转移
27+
3. **Agent 最小架构** — `ReAct + Tools + UI` 公式拆解
28+
4. **最小 Agent 实现** — 天气查询 Demo,40 行核心 Loop
29+
5. **Mini Claude Code 设计** — 拆解 Claude Code,引入 Vercel AI SDK
30+
6. **工程经验** — 上下文管理、安全防护、系统提示词架构
31+
32+
完整教案:[Issue #2](https://github.com/qiniu/techcamp/issues/70)
33+
34+
## 项目结构
35+
36+
```
37+
mini-claude-code/
38+
├── projects/
39+
│ ├── agent-loop/ # 最小 Agent Demo:天气查询(Bun + TypeScript)
40+
│ │ ├── main.ts # 40 行 AgentLoop 核心实现
41+
│ │ ├── tools.ts # 工具定义(获取时间、查询天气)
42+
│ │ └── prompt.md # ReAct 格式系统提示词
43+
│ └── mini-claude-code/ # Mini Claude Code 完整实现
44+
│ ├── src/ # 核心源码
45+
│ └── docs/ # 设计文档
46+
└── README.md
47+
```
48+
49+
## 快速开始
50+
51+
**运行最小 Agent Demo(天气查询):**
52+
53+
```bash
54+
cd projects/agent-loop
55+
bun install
56+
bun main.ts
57+
```
58+
59+
**运行 Mini Claude Code:**
60+
61+
```bash
62+
cd projects/mini-claude-code
63+
bun install
64+
bun src/index.ts
65+
```
66+
67+
> 需要配置 `QINIU_API_KEY` 等对应的环境变量,详见具体项目内容。
68+
69+
## Mini Claude Code 工具集
70+
71+
精简到 4 个核心工具,够用不乱:
72+
73+
| 工具 | 说明 |
74+
| ------------ | --------------- |
75+
| Read | 读取文件内容 |
76+
| Write / Edit | 写入与修改文件 |
77+
| Bash | 执行 Shell 命令 |
78+
| WebFetch | 网络请求 |
79+
80+
> 工具别贪多。每多一个,模型负担就加重。
81+
82+
## 技术栈
83+
84+
- **Runtime**:[Bun](https://bun.sh/)
85+
- **Language**:TypeScript
86+
- **AI SDK**:[Vercel AI SDK](https://ai-sdk.dev/)
87+
- **模型服务**:兼容 OpenAI 协议的任意服务(演示使用七牛大模型推理)
88+
89+
## 相关链接
90+
91+
- [完整教案 Issue #70](https://github.com/qiniu/techcamp/issues/70)
92+
- [Vercel AI SDK 最小用法 Issue #71](https://github.com/qiniu/techcamp/issues/71)
93+
- [Mini Claude Code 设计文档](./projects/mini-claude-code/docs)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# 最小 Agent:天气查询(Bun + TypeScript)
2+
3+
## 文件
4+
5+
```txt
6+
projects
7+
├── bun.lock
8+
├── tools.ts
9+
├── prompt.md
10+
├── package.json
11+
├── tsconfig.json
12+
└── main.ts
13+
```
14+
15+
## 安装
16+
17+
```bash
18+
cd projects
19+
bun install
20+
```
21+
22+
## 环境变量
23+
24+
由外部注入:
25+
26+
- `DEEPSEEK_API_KEY`(必填)
27+
28+
## 运行
29+
30+
```bash
31+
bun run main.ts "上海现在天气如何?"
32+
```
33+
34+
不传问题会使用默认问题:`上海现在天气如何?`
35+
36+
运行时会打印每一轮:
37+
38+
- `[LLM 第N轮输出]`:模型原始输出
39+
- `<observation>...</observation>`:当本轮有工具调用时,紧跟在 `<action>` 后输出工具返回结果
40+
- `=== 最终回答 ===`:最终回答
41+
42+
## XML 协议
43+
44+
- 工具调用:`<action tool="getWeather">{"city":"上海","time":"2026-02-27 10:00"}</action>`
45+
- 最终回答:`<final>...</final>`
46+
47+
## 说明
48+
49+
- 当前仅包含 2 个工具:`getWeather`(模拟天气)和 `getTime`(当前时间)。
50+
- 天气工具返回本地 mock 数据,不调用真实天气 API。

‎2026/01. 从0构建一个Mini Claude Code(TypeScript)/projects/agent-loop/bun.lock‎

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { TOOLKIT, type ToolName } from "./tools"
2+
3+
type Role = "system" | "user" | "assistant"
4+
5+
type ChatMessage = {
6+
role: Role
7+
content: string
8+
}
9+
10+
type ParsedAssistant = {
11+
action?: { tool: string; input: string }
12+
final?: string
13+
}
14+
15+
type DeepSeekMessage = { content?: string }
16+
type DeepSeekChoice = { message?: DeepSeekMessage }
17+
type DeepSeekResponse = { choices?: DeepSeekChoice[] }
18+
19+
/**
20+
* 调用 DeepSeek API 获取模型回复
21+
* @param messages 对话消息列表,包含 system、user 和 assistant 角色的消息
22+
* @returns 模型生成的回复文本
23+
* @throws 如果 API 请求失败或返回格式不正确,将抛出错误
24+
*/
25+
async function callLLMs(messages: ChatMessage[]): Promise<string> {
26+
const res = await fetch("https://api.deepseek.com/v1/chat/completions", {
27+
method: "POST",
28+
headers: {
29+
"Content-Type": "application/json",
30+
Authorization: `Bearer ${process.env.DEEPSEEK_API_KEY}`,
31+
},
32+
body: JSON.stringify({
33+
model: "deepseek-chat",
34+
messages,
35+
temperature: 0.35,
36+
}),
37+
})
38+
39+
if (!res.ok) {
40+
const text = await res.text()
41+
throw new Error(`DeepSeek API 错误: ${res.status} ${text}`)
42+
}
43+
44+
const data = (await res.json()) as DeepSeekResponse
45+
const content = data.choices?.[0]?.message?.content
46+
if (typeof content !== "string") {
47+
throw new Error("DeepSeek 返回内容为空")
48+
}
49+
return content
50+
}
51+
52+
/**
53+
* 解析 Assistant 回复,提取工具调用信息或最终回答
54+
* @param content Assistant 回复的文本内容,可能包含 <action> 或 <final> 标签
55+
* @returns ParsedAssistant 对象,包含 action(工具调用信息)或 final(最终回答)字段
56+
*/
57+
function parseAssistant(content: string): ParsedAssistant {
58+
const actionMatch = content.match(
59+
/<action[^>]*tool="([^"]+)"[^>]*>([\s\S]*?)<\/action>/i,
60+
)
61+
const finalMatch = content.match(/<final>([\s\S]*?)<\/final>/i)
62+
63+
const parsed: ParsedAssistant = {}
64+
if (actionMatch) {
65+
parsed.action = {
66+
tool: actionMatch[1] as ToolName,
67+
input: actionMatch[2]?.trim() ?? "",
68+
}
69+
}
70+
if (finalMatch) {
71+
parsed.final = finalMatch[1]?.trim()
72+
}
73+
74+
return parsed
75+
}
76+
77+
/**
78+
* Agent 主循环,负责与 LLM 交互、解析回复、调用工具并更新对话历史
79+
* @param question
80+
* @returns 最终回答字符串,或错误提示
81+
*/
82+
async function AgentLoop(question: string) {
83+
const systemPrompt = await Bun.file("prompt.md").text()
84+
85+
const history: ChatMessage[] = [
86+
{ role: "system", content: systemPrompt },
87+
{ role: "user", content: question },
88+
]
89+
90+
for (let step = 0; step < 10; step++) {
91+
const assistantText = await callLLMs(history)
92+
console.log(`\n[LLM 第 ${step + 1} 轮输出]\n${assistantText}\n`)
93+
history.push({ role: "assistant", content: assistantText })
94+
95+
const parsed = parseAssistant(assistantText)
96+
if (parsed.final) {
97+
return parsed.final
98+
}
99+
100+
if (parsed.action) {
101+
const toolFn = TOOLKIT[parsed.action.tool as ToolName]
102+
let observation: string
103+
104+
if (toolFn) {
105+
observation = await toolFn(parsed.action.input)
106+
} else {
107+
observation = `未知工具: ${parsed.action.tool}`
108+
}
109+
110+
console.log(`<observation>${observation}</observation>\n`)
111+
112+
history.push({
113+
role: "user",
114+
content: `<observation>${observation}</observation>`,
115+
})
116+
continue
117+
}
118+
119+
break // 未产生 action 或 final
120+
}
121+
122+
return "未能生成最终回答,请重试或调整问题。"
123+
}
124+
125+
/**
126+
* 程序入口,读取用户问题,调用 AgentLoop 获取回答并输出
127+
*/
128+
async function main() {
129+
const userQuestion = process.argv.slice(2).join(" ") || "上海现在天气如何?"
130+
console.log(`用户问题: ${userQuestion}`)
131+
132+
try {
133+
const answer = await AgentLoop(userQuestion)
134+
console.log("\n=== 最终回答 ===")
135+
console.log(answer)
136+
} catch (err) {
137+
console.error(`运行失败: ${(err as Error).message}`)
138+
}
139+
}
140+
141+
await main()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "demo-agent",
3+
"module": "main.ts",
4+
"type": "module",
5+
"private": true,
6+
"devDependencies": {
7+
"@types/bun": "latest"
8+
},
9+
"peerDependencies": {
10+
"typescript": "^5"
11+
}
12+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
你是天气查询的工具型助手,回答要简洁。
2+
可用工具(action 的 tool 属性需与下列名称一致):
3+
4+
- getTime: 返回当前 time 字符串,参数为空。
5+
- getWeather: 返回模拟天气信息字符串,参数为 JSON,如 {"city":"上海","time":"2026-02-27 10:00"}。
6+
7+
回复格式(严格使用 XML,小写标签):
8+
<thought>对问题的简短思考</thought>
9+
<action tool="工具名">工具输入</action> <!-- 若需要工具 -->
10+
等待 <observation> 后再继续思考。
11+
如果已可直接回答,则输出:
12+
<final>最终回答(中文,必要时引用数据来源)</final>
13+
14+
规则:
15+
16+
- 每次仅调用一个工具;工具输入要尽量具体。
17+
- 当用户只问“现在几点”时,优先调用 getTime。
18+
- 查询天气时,必须调用 getWeather,并提供 city 和 time 两个字段。
19+
- 如果拿到 observation 后有了答案,应输出 <final> 而不是重复调用。
20+
- 未知工具时要说明,但仍用 XML 格式。
21+
- 避免幻觉,不确定时请说明。

0 commit comments

Comments
 (0)