@swifty.js/swifty 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -0
- package/dist/agent-MICFDGUF.js +4 -0
- package/dist/anthropic-JDAGNAPR.js +4 -0
- package/dist/checker-PF3FEOF2.js +4 -0
- package/dist/chunk-4KVSJNS6.js +90 -0
- package/dist/chunk-6ARDOHBL.js +4 -0
- package/dist/chunk-7MHXMDYC.js +35 -0
- package/dist/chunk-F6HLYUZ4.js +355 -0
- package/dist/chunk-FZPTNGTU.js +4 -0
- package/dist/chunk-LPLPMGWW.js +336 -0
- package/dist/chunk-MQ5XOYLD.js +4 -0
- package/dist/chunk-RD3MICOU.js +4 -0
- package/dist/{cleanup-4R3534Z3.js → cleanup-BQJDUOKA.js} +1 -1
- package/dist/glob_addon.node +0 -0
- package/dist/main.js +240 -240
- package/dist/openai-I4WTRNNT.js +26 -0
- package/dist/{server-XPKSROM2.js → server-OGPKL2U2.js} +22 -21
- package/package.json +17 -24
- package/dist/anthropic-737END4X.js +0 -4
- package/dist/chunk-NCPOPA4A.js +0 -4
- package/dist/chunk-SC34YHMX.js +0 -521
- package/dist/chunk-Z3E5YV3P.js +0 -121
- package/dist/openai-4KX74QBZ.js +0 -27
- package/docs/ch1.md +0 -25
- package/docs/ch10.md +0 -122
- package/docs/ch11.md +0 -163
- package/docs/ch12.md +0 -289
- package/docs/ch13.md +0 -320
- package/docs/ch14.md +0 -152
- package/docs/ch15.md +0 -547
- package/docs/ch2.md +0 -273
- package/docs/ch3.md +0 -206
- package/docs/ch4.md +0 -125
- package/docs/ch5.md +0 -165
- package/docs/ch6.md +0 -201
- package/docs/ch7.md +0 -448
- package/docs/ch8.md +0 -217
- package/docs/ch9.md +0 -351
- package/docs/index.css +0 -23
- package/docs/index.md +0 -21
- package/docs/swifty.mdx +0 -7
package/docs/ch2.md
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
# LLM API、对话管理
|
|
2
|
-
|
|
3
|
-
请求 Demo
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
# Anthropic
|
|
7
|
-
curl https://api.anthropic.com/v1/messages \
|
|
8
|
-
-H 'Content-Type: application/json' \
|
|
9
|
-
-H 'anthropic-version: 2023-06-01' \
|
|
10
|
-
-H "X-Api-Key: $ANTHROPIC_API_KEY" \
|
|
11
|
-
-d '{
|
|
12
|
-
"max_tokens": 1024,
|
|
13
|
-
"model": "claude-sonnet-4-6",
|
|
14
|
-
"messages": [
|
|
15
|
-
{
|
|
16
|
-
"role": "user",
|
|
17
|
-
"content": "Hello claude."
|
|
18
|
-
}
|
|
19
|
-
]
|
|
20
|
-
}'
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
响应 Demo
|
|
24
|
-
|
|
25
|
-
<!-- 源码: src/llm/anthropic.ts (usage 字段) -->
|
|
26
|
-
|
|
27
|
-
```json
|
|
28
|
-
{
|
|
29
|
-
"id": "msg_abcdefghijklmn0123456789",
|
|
30
|
-
"type": "message",
|
|
31
|
-
"role": "assistant",
|
|
32
|
-
"content": [
|
|
33
|
-
{
|
|
34
|
-
"type": "text",
|
|
35
|
-
"text": "Hello! How can I assist you today?"
|
|
36
|
-
}
|
|
37
|
-
],
|
|
38
|
-
"model": "claude-sonnet-4-6",
|
|
39
|
-
"stop_reason": "end_turn",
|
|
40
|
-
"usage": {
|
|
41
|
-
"input_tokens": 10,
|
|
42
|
-
"output_tokens": 12,
|
|
43
|
-
"cache_read_input_tokens": 0,
|
|
44
|
-
"cache_creation_input_tokens": 0
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
- 请求的 messages: 每条 message 有 role 和 content 两个字段, role (API 请求场景下) 只有两个值: user 和 assistant; messages 数组中, 最好保持 user 和 assistant 两个 role 交替出现; 如果连续传递两条 user 消息, API 不会报错, 会自动合并为一条 user 消息
|
|
50
|
-
- LLM 返回一个工具调用 (tool_use) 请求, 这是 assistant 消息; 用户调用工具拿到结果, 该工具调用结果需要作为 user 消息发送; 如果错误的将工具调用结果作为 assistant 消息发送, 则会导致连续两条 assistant 消息, API 会直接报错
|
|
51
|
-
- 响应的 content 字段是一个数组: LLM 的响应可能包含多种内容, 每种内容是一个独立的 content block, 类型可能是 text、tool_use 等
|
|
52
|
-
- 流式响应基于 SSE (Server-Sent Events), 本质是 HTTP 长连接
|
|
53
|
-
|
|
54
|
-
Claude 的流式事件有固定顺序
|
|
55
|
-
|
|
56
|
-
<!-- 源码: src/llm/anthropic.ts (SSE 事件处理) -->
|
|
57
|
-
|
|
58
|
-
```txt
|
|
59
|
-
message_start 整个响应开始, 携带 input_tokens 输入 token 数、cache_read_input_tokens、cache_creation_input_tokens
|
|
60
|
-
content_block_start 一个内容块开始 (thinking 推理、text 文本或 tool_use 工具调用), 一个响应可能有多个 content_block 内容块
|
|
61
|
-
content_block_delta 内容块的内容增量, delta.type 有 4 种:
|
|
62
|
-
text_delta 文本增量, 每到达一个词, 可以将内容增量提交给 UI 渲染
|
|
63
|
-
thinking_delta 推理增量
|
|
64
|
-
input_json_delta 工具调用参数增量
|
|
65
|
-
signature_delta 签名增量
|
|
66
|
-
content_block_stop 一个内容块结束
|
|
67
|
-
message_delta 消息增量 (output_tokens 输出 token 数, stop_reason 停止原因)
|
|
68
|
-
message_stop 整个响应结束
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
<!-- 源码: src/llm/events.ts (StreamEvent) -->
|
|
72
|
-
|
|
73
|
-
封装层将 LLM API 的 SSE 事件映射到 CLI 的 StreamEvent:
|
|
74
|
-
|
|
75
|
-
```txt
|
|
76
|
-
content_block_delta (text_delta) -> text_delta
|
|
77
|
-
content_block_delta (thinking_delta) -> thinking_delta
|
|
78
|
-
content_block_stop -> thinking_complete
|
|
79
|
-
content_block_start (tool_use) -> tool_call_start
|
|
80
|
-
content_block_delta (input_json_delta) -> tool_call_delta
|
|
81
|
-
content_block_stop -> tool_call_complete
|
|
82
|
-
message_stop -> stream_end
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## 请求的 system, messages, tools
|
|
86
|
-
|
|
87
|
-
- system 参数存放用户信息和环境信息, 包括: 你是谁、操作系统是什么、工作目录是什么
|
|
88
|
-
- messages 参数存放对话历史、上下文窗口
|
|
89
|
-
- tools 参数存放工具描述
|
|
90
|
-
|
|
91
|
-
<!-- 源码: src/llm/anthropic.ts -->
|
|
92
|
-
<!-- 源码: src/tools/read-file.ts (ReadFile 工具定义、描述) -->
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"model": "claude-sonnet-4-6",
|
|
97
|
-
"max_tokens": 4096,
|
|
98
|
-
"system": [
|
|
99
|
-
{
|
|
100
|
-
"type": "text",
|
|
101
|
-
"text": "You are Swifty, a terminal AI programming assistant.\n\n# Environment\nOperating System: MacOS\nWorking Directory: /path/to/cwd\nCurrent Time: 2026-06-22",
|
|
102
|
-
"cache_control": { "type": "ephemeral" }
|
|
103
|
-
}
|
|
104
|
-
],
|
|
105
|
-
"messages": [
|
|
106
|
-
{ "role": "user", "content": "Explain the contents of ./app.ts." },
|
|
107
|
-
{
|
|
108
|
-
"role": "assistant",
|
|
109
|
-
"content": "Sure, let me read the contents of ./app.ts.\nfunction main() {\n console.log(\"javascript newbie\")\n}"
|
|
110
|
-
},
|
|
111
|
-
{ "role": "user", "content": "What functions are in this file?" }
|
|
112
|
-
],
|
|
113
|
-
"tools": [
|
|
114
|
-
{
|
|
115
|
-
"name": "ReadFile",
|
|
116
|
-
"description": "Read a file and return its contents with line numbers.\n\nUsage Notes\n\n- The file_path should be an absolute path when possible.\n- By default reads up to 2000 lines from the beginning of the file.\n- Use offset and limit to read specific parts of large files.",
|
|
117
|
-
"input_schema": {
|
|
118
|
-
"type": "object",
|
|
119
|
-
"properties": {
|
|
120
|
-
"file_path": {
|
|
121
|
-
"type": "string",
|
|
122
|
-
"description": "Absolute path to the file"
|
|
123
|
-
},
|
|
124
|
-
"offset": {
|
|
125
|
-
"type": "integer",
|
|
126
|
-
"description": "Line number to start from (0-based)",
|
|
127
|
-
"default": 0
|
|
128
|
-
},
|
|
129
|
-
"limit": {
|
|
130
|
-
"type": "integer",
|
|
131
|
-
"description": "Max lines to read",
|
|
132
|
-
"default": 2000
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
"required": ["file_path"]
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
]
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
<!-- 源码: src/llm/anthropic.ts (最后一个 tool 标记 cache_control) -->
|
|
143
|
-
|
|
144
|
-
### OpenAI 兼容
|
|
145
|
-
|
|
146
|
-
<!-- 源码: src/llm/openai.ts (OpenAIClient.stream) -->
|
|
147
|
-
|
|
148
|
-
```json
|
|
149
|
-
{
|
|
150
|
-
"model": "gpt-4.1",
|
|
151
|
-
"max_output_tokens": 4096, // 使用 max_output_tokens 而不是 max_tokens
|
|
152
|
-
"input": [
|
|
153
|
-
// 使用 input 而不是 messages
|
|
154
|
-
{ "role": "system", "content": "You are a helpful assistant." },
|
|
155
|
-
{ "role": "user", "content": "Hello" }
|
|
156
|
-
],
|
|
157
|
-
"stream": true
|
|
158
|
-
}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
OpenAI 没有 prompt cache 的 cache_control, cache_read 通过 `usage.input_tokens_details.cached_tokens` 返回
|
|
162
|
-
|
|
163
|
-
<!-- 源码: src/llm/openai.ts (OpenAI cache_read 解析) -->
|
|
164
|
-
|
|
165
|
-
## token
|
|
166
|
-
|
|
167
|
-
token 是 LLM 的计费单位, 每个英文单词约 1-2 个 token, 每个汉字约 1-2 个 token, 具体取决于 LLM 使用的 tokenizer
|
|
168
|
-
|
|
169
|
-
Claude API 的计费分为
|
|
170
|
-
|
|
171
|
-
- inputTokens: 未命中 prompt cache 的输入 token 数, 即发送给 LLM 的内容, 即 system_prompt, tools 描述和 messages 中未命中 prompt cache 的输入
|
|
172
|
-
- outputTokens: 输出 token 数, 即 LLM 生成的内容, 输出 token 比输入 token 贵的多
|
|
173
|
-
- cacheReadInputTokens: 命中 prompt cache 的输入 token 数, 价格远低于普通 input_tokens
|
|
174
|
-
- cacheCreationInputTokens: 创建 prompt cache 的输入 token 数, 价格略高于普通 input_tokens
|
|
175
|
-
|
|
176
|
-
<!-- 源码: src/llm/events.ts (UsageInfo 接口, 包含 4 个 token 字段) -->
|
|
177
|
-
|
|
178
|
-
### 历史越长、输入越贵
|
|
179
|
-
|
|
180
|
-
每轮请求, 都需要发送完整的对话历史; 如果和 LLM 聊了 20 轮, 第 21 轮请求会包含前 20 轮的所有消息; input_tokens 会随着对话轮次线形增长, 所以需要上下文压缩
|
|
181
|
-
|
|
182
|
-
## Extend Thinking 推理
|
|
183
|
-
|
|
184
|
-
Claude 支持 Extended Thinking, 让 LLM 回复前先进行内部推理, 开启后响应的 content 数组中会多一个 `type: thinking` 的内容块, 排在 text 内容块的前面; thinking 的 token 计算到 output_tokens 中;
|
|
185
|
-
|
|
186
|
-
包含工具调用的一轮对话, 对话历史中的 thinking 内容块必须携带, 和后面的 tool_result 一起发送给 LLM API, 否则会报错; 对于纯聊天、没有工具调用的场景, 则对话历史中的 thinking 内容块可以不携带, LLM API 会自动忽略
|
|
187
|
-
|
|
188
|
-
<!-- 源码: src/conversation/conversation.ts (ThinkingBlock 接口: thinking + signature) -->
|
|
189
|
-
<!-- 源码: src/llm/anthropic.ts (assistant 消息 thinking block 的 API 转换) -->
|
|
190
|
-
|
|
191
|
-
## 如何封装
|
|
192
|
-
|
|
193
|
-
ProviderConfig 有 8 个字段, 覆盖主流厂商: Anthropic、OpenAI、OpenAI 兼容层
|
|
194
|
-
|
|
195
|
-
<!-- 源码: src/config/config.ts (ProviderConfigSchema) -->
|
|
196
|
-
|
|
197
|
-
- name: provider 名称
|
|
198
|
-
- protocol: LLM API 协议, 枚举值 "anthropic" | "openai" | "openai-compat"
|
|
199
|
-
- base_url: 端点地址
|
|
200
|
-
- model: LLM 模型, 例如 claude-haiku-4-6、claude-sonnet-4-6、claude-opus-4-6
|
|
201
|
-
- api_key: 令牌
|
|
202
|
-
- thinking: 是否开启 extended thinking (可选)
|
|
203
|
-
- context_window: 上下文窗口大小 (可选, 默认 200_000)
|
|
204
|
-
- max_output_tokens: 最大输出 token 数 (可选, thinking 开启时默认 200_000, 关闭时默认 128_000)
|
|
205
|
-
|
|
206
|
-
<!-- 源码: src/config/config.ts (getMaxOutputTokens 默认值逻辑) -->
|
|
207
|
-
<!-- 源码: src/config/config.ts (DEFAULT_CONTEXT_WINDOW = 200_000) -->
|
|
208
|
-
|
|
209
|
-
封装层负责翻译
|
|
210
|
-
|
|
211
|
-
## 多轮对话如何实现
|
|
212
|
-
|
|
213
|
-
每一轮 LLM API 请求, 都包含完整的对话历史, 需要在客户端维护完整的消息列表, 每次用户 (CLI) 发送请求、LLM 响应, 都需要记录, token 消耗会随着对话轮次线形增长
|
|
214
|
-
|
|
215
|
-
### 消息模型
|
|
216
|
-
|
|
217
|
-
<!-- 源码: src/conversation/conversation.ts (Message 接口) -->
|
|
218
|
-
|
|
219
|
-
内部 Message 接口:
|
|
220
|
-
|
|
221
|
-
```ts
|
|
222
|
-
interface Message {
|
|
223
|
-
role: "user" | "assistant" | "system";
|
|
224
|
-
content: string;
|
|
225
|
-
thinkingBlocks?: ThinkingBlock[];
|
|
226
|
-
toolUses?: ToolUseBlock[];
|
|
227
|
-
toolResults?: ToolResultBlock[];
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
- role: user, assistant, system
|
|
232
|
-
- content: 消息内容
|
|
233
|
-
- thinkingBlocks: 可选, assistant 消息的 extended thinking 推理块 (包含 thinking 文本和 signature)
|
|
234
|
-
- toolUses: 可选, assistant 消息中的工具调用请求列表 (包含 toolUseId、toolName、arguments)
|
|
235
|
-
- toolResults: 可选, user 消息中的工具调用结果列表 (包含 toolUseId、content、isError)
|
|
236
|
-
|
|
237
|
-
内部层
|
|
238
|
-
|
|
239
|
-
- ID: `msg_<hash>` 可以根据消息 ID 定位到正在接收的 assistant 消息, 并追加 SSE chunk
|
|
240
|
-
- status: streaming, complete, error 封装层翻译时可以过滤 error 状态的消息
|
|
241
|
-
- timestamp
|
|
242
|
-
- usage: `{ inputTokens, outputTokens, cacheReadInputTokens, cacheCreationInputTokens }`
|
|
243
|
-
- role: user, assistant, system, tool (Only for OpenAI)
|
|
244
|
-
- content: 消息内容 (thinking 推理、text 文本、tool_use 工具调用或其他...)
|
|
245
|
-
|
|
246
|
-
<!-- 源码: src/llm/openai.ts (buildChatCompletionMessages) -->
|
|
247
|
-
|
|
248
|
-
### 对话管理器
|
|
249
|
-
|
|
250
|
-
考虑到流式接收, CLI 正在向 assistant 消息 (SSE chunk 数组) 中追加 SSE chunk, 同时 TUI 正在读 assistant 消息 (SSE chunk 数组) 以渲染 TUI, 两处同时操作同一个列表, 可能导致数据竞争
|
|
251
|
-
|
|
252
|
-
- Node.js 单线程异步, 避免数据竞争
|
|
253
|
-
- Go 加锁
|
|
254
|
-
|
|
255
|
-
追加 SSE chunk 时, 拿到 assistant 消息的唯一 ID, 根据 ID 追加 SSE chunk; 更新 TUI 时, 根据 ID 拿到一份 assistant 消息的快照
|
|
256
|
-
|
|
257
|
-
## 格式转换: 内部消息到 LLM API 消息
|
|
258
|
-
|
|
259
|
-
- [Anthropic](../src/llm/anthropic.ts)
|
|
260
|
-
- [OpenAI](../src/llm/openai.ts)
|
|
261
|
-
|
|
262
|
-
<!-- 源码: src/llm/anthropic.ts (buildAnthropicMessages) -->
|
|
263
|
-
|
|
264
|
-
1. 转换: 内部 Message 的 thinkingBlocks、content、toolUses 转换为 LLM API 的 content block; toolResults 转换为 LLM API 的 tool_result block
|
|
265
|
-
2. 合并: 虽然 Claude API 可以自动合并相邻的相同 role 的消息, 但是在客户端合并是更好的做法, 消息结构更清晰, 减少 token 消耗
|
|
266
|
-
3. 过滤后第一条消息的 role 必须是 user, 并且 message 数组中 user 和 assistant 两个 role 交替出现
|
|
267
|
-
|
|
268
|
-
### 流式接收机制
|
|
269
|
-
|
|
270
|
-
<!-- 源码: src/llm/anthropic.ts (AnthropicClient.stream, AsyncGenerator) -->
|
|
271
|
-
<!-- 源码: src/llm/events.ts (StreamEvent 联合类型) -->
|
|
272
|
-
|
|
273
|
-
用户发送请求后, ConversationManager 的 stream 方法返回 `AsyncGenerator<StreamEvent>`, 每收到一个 SSE chunk, yield 对应的 StreamEvent; CLI 循环消费 AsyncGenerator, 如果是文本增量, 则提交给 UI 渲染, 如果收集到完整的工具调用 json 参数, 则调用工具; 流式接收完成后, agent 循环调用 ConversationManager 的 appendMessages 方法将消息写入对话历史
|
package/docs/ch3.md
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
# 工具调用
|
|
2
|
-
|
|
3
|
-
## 告诉 LLM 有哪些工具
|
|
4
|
-
|
|
5
|
-
调用 LLM API 时, 可以通过 tools 参数告诉 LLM 有哪些工具, 包括名称 name, 描述 description, 参数格式 tool schema
|
|
6
|
-
|
|
7
|
-
<!-- 源码: src/tools/descriptions.ts, src/tools/read-file.ts -->
|
|
8
|
-
|
|
9
|
-
```json
|
|
10
|
-
{
|
|
11
|
-
"tools": [
|
|
12
|
-
{
|
|
13
|
-
"name": "ReadFile",
|
|
14
|
-
"description": "Read a file and return its contents with line numbers.\n\nUsage Notes\n\n- The file_path should be an absolute path when possible.\n- By default reads up to 2000 lines from the beginning of the file.\n- Use offset and limit to read specific parts of large files. Only read what you need.\n- Results are returned with line numbers (1-based) for easy reference.\n- This tool can only read files, not directories. Use glob to list directory contents.\n- Do NOT re-read a file you just edited to verify -- EditFile would have errored if the change failed.",
|
|
15
|
-
"input_schema": {
|
|
16
|
-
"type": "object",
|
|
17
|
-
"properties": {
|
|
18
|
-
"file_path": {
|
|
19
|
-
"type": "string",
|
|
20
|
-
"description": "Absolute path to the file"
|
|
21
|
-
},
|
|
22
|
-
"offset": {
|
|
23
|
-
"type": "integer",
|
|
24
|
-
"description": "Line number to start from (0-based)",
|
|
25
|
-
"default": 0
|
|
26
|
-
},
|
|
27
|
-
"limit": {
|
|
28
|
-
"type": "integer",
|
|
29
|
-
"description": "Max lines to read",
|
|
30
|
-
"default": 2000
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
"required": ["file_path"]
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
]
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## LLM 决定调用工具
|
|
41
|
-
|
|
42
|
-
LLM 决定调用工具时, LLM API 的响应中包含一个结构化的工具调用请求
|
|
43
|
-
|
|
44
|
-
```json
|
|
45
|
-
{
|
|
46
|
-
"role": "assistant",
|
|
47
|
-
"content": [
|
|
48
|
-
{
|
|
49
|
-
"type": "text",
|
|
50
|
-
"text": "Let me read the text content of this file."
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
"type": "tool_use",
|
|
54
|
-
"id": "toolu_123abc",
|
|
55
|
-
"name": "ReadFile",
|
|
56
|
-
"input": {
|
|
57
|
-
"file_path": "src/main.go"
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
]
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
- 一个 LLM API 的响应中, 可以既包含文本内容块, 又包含工具调用请求内容块, 甚至同时请求调用多个工具
|
|
65
|
-
- 每个 tool_use 工具调用请求, 都有一个唯一 ID
|
|
66
|
-
|
|
67
|
-
## CLI 执行工具调用, 返回工具调用结果给 LLM API
|
|
68
|
-
|
|
69
|
-
- CLI 收到 tool_use 工具调用请求后, CLI 执行工具调用相关代码, 将工具调用结果返回给 LLM API
|
|
70
|
-
|
|
71
|
-
```jsonc
|
|
72
|
-
{
|
|
73
|
-
// 执行工具调用, role 是 user
|
|
74
|
-
"role": "user",
|
|
75
|
-
"content": [
|
|
76
|
-
{
|
|
77
|
-
"type": "tool_result",
|
|
78
|
-
// 必须和 tool_use 工具调用请求的 ID 相同
|
|
79
|
-
"tool_use_id": "toolu_123abc",
|
|
80
|
-
"content": "1\tfunction main() {\n2\t console.log(\"javascript newbie\")\n3\t}",
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## LLM 继续
|
|
87
|
-
|
|
88
|
-
LLM 收到工具调用结果, 继续...
|
|
89
|
-
|
|
90
|
-
LLM 负责决策, 请求调用工具; CLI 负责执行工具调用, 将工具调用结果返回给 LLM API
|
|
91
|
-
|
|
92
|
-
- LLM 决定是否调用某个工具时, 主要参考工具描述 (description), 工具描述的质量直接决定 LLM 的工具调用行为, 包括: 什么时候调用工具、调用哪个工具、如何传递参数
|
|
93
|
-
- 好的工具描述应该包括: 工具的核心功能、什么时候应该调用、什么时候不应该调用、输入参数的 schema、输出的工具调用结果的 schema、与其他工具的配合 (工作流建议, 例如对于大文件, 先 Grep 定位再 ReadFile 读文件)
|
|
94
|
-
|
|
95
|
-
## 工具接口设计
|
|
96
|
-
|
|
97
|
-
<!-- 源码: src/tools/types.ts -->
|
|
98
|
-
|
|
99
|
-
- 身份信息: name, description, schema (input_schema)
|
|
100
|
-
- 元信息
|
|
101
|
-
- category 分类
|
|
102
|
-
- deferred? 延迟加载
|
|
103
|
-
- system? 内部工具
|
|
104
|
-
- concurrencySafe? 是否可以和其他工具并发执行
|
|
105
|
-
- 行为: execute、validateInput?
|
|
106
|
-
|
|
107
|
-
<!-- 源码: src/tools/types.ts -->
|
|
108
|
-
|
|
109
|
-
```ts
|
|
110
|
-
export interface ToolResult {
|
|
111
|
-
output: string;
|
|
112
|
-
// 工具执行失败对于 LLM 是有价值的反馈, 提示 LLM 调整策略
|
|
113
|
-
isError: boolean;
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### ReadFile
|
|
118
|
-
|
|
119
|
-
<!-- 源码: src/tools/read-file.ts -->
|
|
120
|
-
|
|
121
|
-
- properties: file_path, offset, limit
|
|
122
|
-
- 元信息: 只读、非破坏性, `category: read`
|
|
123
|
-
- 行号: 读文件需要带行号前缀, 方便定位代码位置 `"1\tfunction main() {\n2\t console.log(\"javascript newbie\")\n3\t}"`
|
|
124
|
-
- 大文件: 支持 offset 和 limit 参数, offset 默认 0, limit 默认 2000, 指定从第 offset 行开始读、读 limit 行, 使得 LLM 可以分段读文件
|
|
125
|
-
- 二进制文件: 通过读文件的前 512 字节, 如果包含 NUL 字符 (\x00), 则判定为二进制文件并拒绝读取, 提示 LLM 使用 bash 工具处理
|
|
126
|
-
|
|
127
|
-
### WriteFile
|
|
128
|
-
|
|
129
|
-
<!-- 源码: src/tools/write-file.ts -->
|
|
130
|
-
|
|
131
|
-
- properties: file_path, content
|
|
132
|
-
- 元信息: 非只读、非破坏性, `category: write`
|
|
133
|
-
- 创建或重写, 创建时需要递归的创建父目录
|
|
134
|
-
|
|
135
|
-
### EditFile
|
|
136
|
-
|
|
137
|
-
<!-- 源码: src/tools/edit-file.ts -->
|
|
138
|
-
|
|
139
|
-
- properties: file_path, old_string, new_string, replace_all
|
|
140
|
-
- 元信息: 非只读、非破坏性, `category: write`
|
|
141
|
-
- 如果 replace_all === false, 则 old_string 必须唯一匹配
|
|
142
|
-
- 如果匹配多个, 报错提示: 该 old_string 匹配 N 个, 请提供更多的上下文使得 old_string 唯一匹配
|
|
143
|
-
- 如果没有找到, 说明 LLM 记忆的文件内容可能过时
|
|
144
|
-
- 替换成功后, 返回 "Successfully edited ${filePath}", 提供给 LLM 确认修改是否正确
|
|
145
|
-
- new_string 为空, 表示删除 old_string
|
|
146
|
-
|
|
147
|
-
### Bash
|
|
148
|
-
|
|
149
|
-
<!-- 源码: src/tools/bash.ts -->
|
|
150
|
-
|
|
151
|
-
- properties: command, timeout
|
|
152
|
-
- 元信息: 非只读、破坏性, `category: command`
|
|
153
|
-
- 工作目录: 项目根目录, 超时: 120s
|
|
154
|
-
- 输出: stdout、stderr 合并到一个流; 输出过长时截断, 保留前面的部分和截断标记
|
|
155
|
-
- 命令退出码的语义
|
|
156
|
-
- 默认非 0 退出码 isError: true
|
|
157
|
-
- grep 退出码 1 表示: 没有找到匹配, 退出码 >=2 视为 error
|
|
158
|
-
- diff 退出码 1 表示文件有差异, 退出码 >=2 视为 error
|
|
159
|
-
|
|
160
|
-
### Glob
|
|
161
|
-
|
|
162
|
-
<!-- 源码: src/tools/glob.ts -->
|
|
163
|
-
|
|
164
|
-
- properties: pattern, path
|
|
165
|
-
- 元信息: 只读、非破坏性, `category: read`
|
|
166
|
-
- glob 查找文件名
|
|
167
|
-
- 搜索结果按修改时间倒序排序, 最新修改的排在前面
|
|
168
|
-
|
|
169
|
-
### Grep
|
|
170
|
-
|
|
171
|
-
<!-- 源码: src/tools/grep.ts -->
|
|
172
|
-
|
|
173
|
-
- properties: pattern, path, include
|
|
174
|
-
- 元信息: 只读、非破坏性, `category: read`
|
|
175
|
-
- grep 找文件内容
|
|
176
|
-
- 输出格式: 文件路径:行号:匹配的内容
|
|
177
|
-
|
|
178
|
-
<!-- 源码: src/tools/types.ts (ToolCategory = "read" | "write" | "command") -->
|
|
179
|
-
|
|
180
|
-
| 工具 | 分类 | 只读 | 破坏性 | 场景 |
|
|
181
|
-
| --------- | ------- | ---- | ------ | --------------- |
|
|
182
|
-
| ReadFile | read | 是 | 否 | 读文件 |
|
|
183
|
-
| WriteFile | write | 否 | 否 | 创建或重写文件 |
|
|
184
|
-
| EditFile | write | 否 | 否 | 修改文件 |
|
|
185
|
-
| Bash | command | 否 | 是 | 执行 shell 命令 |
|
|
186
|
-
| Glob | read | 是 | 否 | 查找文件名 |
|
|
187
|
-
| Grep | read | 是 | 否 | 查找文件内容 |
|
|
188
|
-
|
|
189
|
-
## 流式 tool_use 解析: 拼接 partialJson JSON 碎片
|
|
190
|
-
|
|
191
|
-
```txt
|
|
192
|
-
# tool_use 块开始
|
|
193
|
-
content_block_start -> type: "tool_use", id: "toolu_123abc", name: "ReadFile"
|
|
194
|
-
|
|
195
|
-
# 传输 JSON 碎片
|
|
196
|
-
content_block_delta -> type: "input_json_delta", partial_json: "{"
|
|
197
|
-
content_block_delta -> type: "input_json_delta", partial_json: "\"path\""
|
|
198
|
-
content_block_delta -> type: "input_json_delta", partial_json: ": \"/main.js\"}"
|
|
199
|
-
|
|
200
|
-
# tool_use 块结束
|
|
201
|
-
content_block_stop
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
- tool_use 工具调用请求的 role 是 assistant, tool_result 工具调用结果的 role 是 user
|
|
205
|
-
- 一条 assistant 消息可能同时包含 text 内容块和 tool_use 内容块, 必须在同一条 assistant 消息中, 不能拆成两条 assistant 消息
|
|
206
|
-
- 如果一条 assistant 消息包含多个 tool_use 内容块, 即 LLM 请求同时调用多个工具, 则多个 tool_result 内容块必须在同一条 user 消息中, 通过 id 配对
|
package/docs/ch4.md
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# ReAct 和 Agent Loop
|
|
2
|
-
|
|
3
|
-
ReAct (Reasoning + Acting)
|
|
4
|
-
|
|
5
|
-
- Thinking 解释为什么要做这一步 (text)
|
|
6
|
-
- Act 选择调用一个工具 (tool_use)
|
|
7
|
-
- Observe (tool_result) 分析工具调用结果, 决定下一步怎么做
|
|
8
|
-
|
|
9
|
-
## ReAct 对比其他范式
|
|
10
|
-
|
|
11
|
-
- Chain-of-Thought 只推理, 不行动
|
|
12
|
-
- Act-only 只行动, 不推理
|
|
13
|
-
- ReAct 推理与行动交替
|
|
14
|
-
- Plan-then-execute 先生成完整计划, 再逐步执行
|
|
15
|
-
|
|
16
|
-
## Agent Loop
|
|
17
|
-
|
|
18
|
-
<!-- 源码: src/agent/agent.ts -->
|
|
19
|
-
|
|
20
|
-
```js
|
|
21
|
-
function agentLoop(userMessage) {
|
|
22
|
-
const messages = [...historyMessages, userMessage];
|
|
23
|
-
while (true) {
|
|
24
|
-
const response = streamLLM(systemPrompt, messages, toolSchemas);
|
|
25
|
-
const toolUses = response.getToolUses();
|
|
26
|
-
if (toolUses.length === 0) {
|
|
27
|
-
return response;
|
|
28
|
-
}
|
|
29
|
-
messages.push({ role: "assistant", content: response.content });
|
|
30
|
-
const results = [];
|
|
31
|
-
for (const tu of toolUses) {
|
|
32
|
-
const result = executeTool(tu);
|
|
33
|
-
results.push(result);
|
|
34
|
-
}
|
|
35
|
-
messages.push({ role: "user", content: results });
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## 退出 Agent Loop
|
|
41
|
-
|
|
42
|
-
<!-- 源码: src/agent/agent.ts -->
|
|
43
|
-
|
|
44
|
-
Swifty 需要 5 种 agent loop 退出条件
|
|
45
|
-
|
|
46
|
-
1. LLM 决定退出循环: LLM API 响应中没有 tool_use, stop reason 是 end_turn (Anthropic)
|
|
47
|
-
2. 设置最大循环次数, 超过最大循环次数后强制退出循环
|
|
48
|
-
3. 用户按 esc 退出循环: Go 使用 `context.Context`, TS 使用 `AbortController`
|
|
49
|
-
4. 如果 LLM 请求调用的工具不存在或不可用, 则返回错误结果反馈给 LLM 调整; 如果连续请求调用不存在或不可用的工具 (consecutiveUnknown >= 3), 说明 LLM 陷入幻觉, 退出循环
|
|
50
|
-
5. LLM output token 到达 max_tokens: 先提高 output_tokens 到 200_000, 再进行最多 3 轮的多轮恢复, 每轮恢复提示 LLM 从断点继续; 恢复耗尽则降级为正常结束 (TODO: 表述待确认)
|
|
51
|
-
|
|
52
|
-
## AgentEvent 事件流
|
|
53
|
-
|
|
54
|
-
agent loop 期间会发射大量事件:
|
|
55
|
-
|
|
56
|
-
- stream_text: LLM 流式输出的文本增量
|
|
57
|
-
- thinking_text: LLM 推理的文本增量
|
|
58
|
-
- thinking_complete: 完整的推理块, 包含 thinking 和 signature
|
|
59
|
-
- tool_use: LLM 请求调用工具
|
|
60
|
-
- tool_result: LLM 工具调用结束
|
|
61
|
-
- turn_complete: 一轮 LLM 调用结束 (LLM 请求调用工具 + CLI 工具调用结束)
|
|
62
|
-
- loop_complete: 整个 agent loop 结束, stop reason 可以是 end_turn 或 interrupted
|
|
63
|
-
- usage: token 用量更新
|
|
64
|
-
- compact: 上下文压缩完成
|
|
65
|
-
- retry: 自动重试 (output token 到达 max_tokens 恢复或限流等待)
|
|
66
|
-
- error: 发生错误
|
|
67
|
-
|
|
68
|
-
UI 层只需要从 AgentEvent 事件流中消费事件, 根据事件更新 UI, agent 和 UI 解构; Go 使用 channel, TS 使用 AsyncGenerator (`async function* + yield`)
|
|
69
|
-
|
|
70
|
-
## 状态机
|
|
71
|
-
|
|
72
|
-
LLM 响应后, 只有两种可能: 继续循环 / 退出循环
|
|
73
|
-
|
|
74
|
-
```js
|
|
75
|
-
function classifyResponse(response) {
|
|
76
|
-
const toolUses = response.getToolUses();
|
|
77
|
-
if (toolUses.length > 0) {
|
|
78
|
-
return "continue";
|
|
79
|
-
}
|
|
80
|
-
return "terminal";
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## 执行工具
|
|
85
|
-
|
|
86
|
-
按工具的 isConcurrencySafe 分 batch, 并发安全的并发执行, 并发不安全的串行执行; 并发 batch 可以包含多个并发安全的工具调用, 串行 batch 只能包含一个并发不安全的工具调用
|
|
87
|
-
|
|
88
|
-
```js
|
|
89
|
-
/**
|
|
90
|
-
* e.g. LLM returns [Read, Read, Edit, Read, Read]
|
|
91
|
-
*
|
|
92
|
-
* Batches:
|
|
93
|
-
* [Read, Read]
|
|
94
|
-
* [Edit]
|
|
95
|
-
* [Read, Read]
|
|
96
|
-
*/
|
|
97
|
-
function partitionToolCalls(toolUses, registry) {
|
|
98
|
-
const batches = [];
|
|
99
|
-
for (const tu of toolUses) {
|
|
100
|
-
const tool = registry.get(tu.name);
|
|
101
|
-
const safe = tool?.isConcurrencySafe(tu.input) ?? false;
|
|
102
|
-
|
|
103
|
-
if (safe && batches.length > 0) {
|
|
104
|
-
const lastBatch = batches[batches.length - 1];
|
|
105
|
-
lastBatch.calls.push(tu);
|
|
106
|
-
} else {
|
|
107
|
-
batches.push(
|
|
108
|
-
new Batch({
|
|
109
|
-
isConcurrencySafe: safe,
|
|
110
|
-
calls: [tu],
|
|
111
|
-
}),
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return batches;
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
## System Prompt 与环境信息
|
|
120
|
-
|
|
121
|
-
每轮 agent loop turn 都需要发送 System Prompt 给 LLM API, System Prompt 包含用户信息、环境信息 (操作系统、工作目录) 和模式指令 (planMode)
|
|
122
|
-
|
|
123
|
-
## Plan Mode 只规划不做事
|
|
124
|
-
|
|
125
|
-
通过 prompt 约束 LLM 行为, plan mode 的权限矩阵和 default mode 相同: read=allow, write=ask, command=ask, 特殊的是 plan.md 的 write=allow, 不需要用户确认
|