Skip to content

200行代码使用 Function Calling 实现 code-editing agent #49

@xwcoder

Description

@xwcoder

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 实现深度优先遍历树结构
IMAGE

我们看到其成功完成了任务,创建了 traverse.js 并实现了 DFS. 接下来让其将 traverse.js 改为广度优先遍历
IMAGE

同样成功完成了任务。

同样的 idea,我们可以轻松的换成 DeepSeek 实现。那么真正的壁垒也许就在细节的体验打磨、更精准的提示词调试等并不性感的地方。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions