-
Notifications
You must be signed in to change notification settings - Fork 4
Description
Thorsten Ball 在 How to Build an Agent 中展示了如何使用 400 行 go 代码实现一个 code-editing agent。
文章的副标题更有意思 - “皇帝的新衣”。文中提到实现 code-editing agent 所需要的只是 一个 LLM,一个循环和足够的 token。
文中还提到:
This is essentially all there is to the inner loop of a code-editing agent. Sure, integrating it into your editor, tweaking the system prompt, giving it the right feedback at the right time, a nice UI around it, better tooling around the tools, support for multiple agents, and so on — we’ve built all of that in Amp, but it didn’t require moments of genius. All that was required was practical engineering and elbow grease.
我们所需要的是务实的工程设计和"体力活"。
恰巧今天刷到了地平线余凯的访谈,其中谈到了类似的观点:一个好的商业模式不是由聪明的脑袋做的,是由苦活脏活累活,积累很长时间。这样的话,才会有护城河,才有壁垒。
这里,我们将 Thorsten Ball 的 code-editing agent 实现一个 node.js 版本,LLM 服务使用 DeepSeek。借助 OpenAI SDK 只需要 200 行代码。
import * as readline from 'node:readline/promises'
import { stdin, stdout } from 'node:process'
import { inspect } from 'node:util'
import * as fs from 'node:fs'
import OpenAI from 'openai'
const rl = readline.createInterface({
input: stdin,
output: stdout,
})
const client = new OpenAI({
baseURL: 'https://api.deepseek.com',
apiKey: process.env.OPENAI_API_KEY,
});
const readFile = ({ filePath }) => {
return fs.readFileSync(filePath, { encoding: 'utf8' })
}
const listFiles = (arg) => {
const dirPath = arg.dirPath || process.cwd()
return fs.readdirSync(dirPath)
}
const editFile = ({ filePath, old_str, new_str }) => {
if (!filePath || old_str === new_str) {
return 'invalid input parameters'
}
if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, new_str)
return ''
}
const content = fs.readFileSync(filePath, { encoding: 'utf8' })
const newContent = content.replace(old_str, new_str)
fs.writeFileSync(filePath, newContent)
return 'ok'
}
const tools = [
{
type: 'function',
function: {
name: 'read_file',
description: "Read the contents of a given relative file path. Use this when you want to see what's inside a file. Do not use this with directory names.",
parameters: {
type: 'object',
properties: {
filePath: {
type: 'string',
description: 'The relative path of a file in the working directory.',
}
},
required: ['filePath'],
},
}
},
{
type: 'function',
function: {
name: 'list_files',
description: 'List files and directories at a given path. If no path is provided, lists files in the current directory.',
parameters: {
type: 'object',
properties: {
dirPath: {
type: 'string',
description: 'Optional relative path to list files from. Defaults to current directory if not provided.',
}
},
},
}
},
{
type: 'function',
function: {
name: 'edit_file',
description: `Make edits to a text file.
Replaces 'old_str' with 'new_str' in the given file. 'old_str' and 'new_str' MUST be different from each other.
If the file specified with path doesn't exist, it will be created.
`,
parameters: {
type: 'object',
properties: {
filePath: {
type: 'string',
description: 'The path to the file',
},
old_str: {
type: 'string',
description: 'Text to search for - must match exactly and must only have one match exactly',
},
new_str: {
type: 'string',
description: 'Text to replace old_str with',
}
},
required: ['filePath', 'new_str'],
},
}
},
]
const execTool = (id, name, args) => {
if (name === 'read_file') {
const content = readFile(JSON.parse(args))
return {
role: 'tool',
tool_call_id: id,
content,
name,
}
}
if (name === 'list_files') {
const content = listFiles(args ? JSON.parse(args) : undefined)
return {
role: 'tool',
tool_call_id: id,
content: JSON.stringify(content),
name,
}
}
if (name === 'edit_file') {
const content = editFile(JSON.parse(args))
return {
role: 'tool',
tool_call_id: id,
content,
name,
}
}
return null
}
(async function main () {
const conversation = []
let readUserInput = true
console.log('Chat with DeepSeek')
while (1) {
// 处理用户输入
if (readUserInput) {
const content = await rl.question('\u001b[94mYou\u001b[0m: ')
conversation.push({
role: 'user',
content,
})
}
const response = await client.chat.completions.create({
model: 'deepseek-chat',
messages: conversation,
tools,
})
// console.log(inspect(response, { depth: 10 }))
if (!response.choices.length) {
console.log('\u001b[91mError\u001b[0m: DeepSeek response no choices.')
continue
}
const message = response.choices[0].message
conversation.push(message)
if (message.content) {
console.log(`\u001b[93mAI\u001b[0m: ${message.content}\n`)
}
if (!message.tool_calls || !message.tool_calls.length) {
readUserInput = true
continue
}
readUserInput = false
for (const toolCall of message.tool_calls) {
if (toolCall.type !== 'function') {
continue
}
console.log(`\u001b[92mTool Call\u001b[0m: ${toolCall.function.name}(${toolCall.function.arguments})\n`)
const toolOuput = execTool(toolCall.id, toolCall.function.name, toolCall.function.arguments)
if (toolOuput) {
conversation.push(toolOuput)
}
}
}
})()
原文中的示例任务,其都能够很好的完成。我们再看看其他示例。首先让其创建 traverse.js 实现深度优先遍历树结构。
我们看到其成功完成了任务,创建了 traverse.js 并实现了 DFS. 接下来让其将 traverse.js 改为广度优先遍历。
同样成功完成了任务。
同样的 idea,我们可以轻松的换成 DeepSeek 实现。那么真正的壁垒也许就在细节的体验打磨、更精准的提示词调试等并不性感的地方。