回顾

在用python做一个智能编程助手(1)里面,我们已经完成了以下内容:

  1. 申请API_Key和OpenAI_兼容客户端初始化
  2. 使用chat.completions.create()实现大模型调用和第一次对话
  3. 对话使用的系统消息和用户消息
  4. 实现对话循环,实现多轮记忆会话
  5. 记录和显示Token
  6. 流式STREAM与显示推理thinking内容
  7. responses协议调用与Codex的安装和使用

到目前为止,已经实现了一个简版的AI对话智能体(agent),但这个智能体还不够智能,它连我们使用的网页版的AI助手都不如,它只能实现通过大模型已经训练的内容的对话,无法读取本地文件,写文件,执行本地命名,不能上网搜索,不知道当前时间......

很多课程里都会说大模型就像一个大脑,但你需要让Agent给它安装上手和脚,否则它只是个聊天机器人,做不了其他的事情。

我们以一个编程的实际案例来看下要从一个只能聊天的初级Agent如何添加相应的功能升级到可以实现自动化编程和调试的入门级Agent的。

步骤 阶段 操作 说明 人工/LLM 详细描述
1 初始生成 向LLM提出需求 "请用Python写一个冒泡排序程序,包含测试用例,可独立运行" LLM 人工输入提示词,LLM 回复包含完整代码块
2 代码交付 LLM返回代码 LLM 返回含 def bubble_sort() + if __name__ == "__main__" 的完整代码 LLM 返回 markdown 代码块
3 手工复制 选中LLM回复的代码块 鼠标框选或 Ctrl+A 选中代码块内容 人工 注意:代码块标记(python ... )不能一起复制
4 手工复制 粘贴到IDE/编辑器 打开 VS Code、PyCharm 或记事本,Ctrl+V 粘贴 人工 此时文件尚未命名保存在磁盘上
5 手工保存 保存为 .py 文件 Ctrl+S 保存为 bubble_sort.py(需选择目录) 人工 若用IDE则有语法高亮,可初步目视检查缩进
6 执行 在终端/CMD执行脚本 运行 python bubble_sort.py 人工+Bash 终端输出结果或错误信息
7 结果判断 检查输出 若无错误且输出正确排序结果 → 跳到步骤15;若有错误 → 继续 人工 常见错误:IndentationError、NameError、TypeError 等
8 捕获错误 复制终端错误信息 选中完整 traceback(含 File/Line/Error type/Message) 人工 必须复制完整 traceback,不能截断,否则 LLM 无法准确定位
9 发送给LLM 粘贴错误信息并询问修复 "运行出错,以下是错误信息:[粘贴],请修复" 人工+LLM 可额外指定要求,如"保持接口不变"、"添加类型注解"等
10 接收修复 LLM返回修正后的代码 LLM 回复含修复后代码(可能只给出 diff 或完整代码块) LLM 注意:LLM 可能只修改了部分代码,需确认是否完整
11 手工复制 选中修改后的代码块 同步骤3 人工 若 LLM 仅给出 diff,需手动应用到现有文件,更易出错
12 覆盖粘贴 在IDE中全选旧代码并粘贴新代码 Ctrl+A → Ctrl+V 人工 注意:若IDE中已有修改未保存,应先保存或撤销
13 保存 Ctrl+S 保存文件 同步骤5 人工
14 循环 重复步骤6~13 直到运行成功或放弃 循环 典型需要2~4轮;常见陷阱:LLM 修复A问题引入了B问题
15 报告成功 告知LLM运行成功 "程序已成功运行,输出结果为:[粘贴结果]" 人工+LLM 可额外要求生成测试报告或解释代码
16 生成报告 LLM输出总结 LLM 回复运行结论、复杂度分析、优化建议等 LLM 人工可据此完善文档或提交代码

要实现上述 16 步全自动闭环(人只需输入初始需求,无需中间干预),需要 4 个 function calling tools

Tool 名称 功能 对应表格中替代的人工步骤 参数
write_file 将 LLM 输出的代码写入磁盘文件 步骤 35(复制→粘贴→保存)和步骤 1113 path(文件路径)、content(文件内容)、mode(write/append)
execute_command 在 shell/bash 中执行命令并返回 stdout/stderr/exit_code 步骤 6(运行脚本) command(命令字符串)、timeout(超时秒数)
read_file 读取文件内容,用于确认当前代码状态或辅助调试 可选,用于检查保存结果或检查文件中是否有残留错误 path(文件路径)
report_result 将最终成功结果或失败原因结构化地返回给 LLM 步骤 8(捕获错误)、步骤 15(告知成功) status("success"/"error")、stdoutstderrexit_code

02_Function-Calling工具

2023 年 6 月 13 日 OpenAI 公布了 Function Call(函数调用)功能,该功能指的是在语言模型中集成外部功能或 API 调用能力,这意味着模型可以在生成文本的过程中调用外部函数或服务,获取额外的数据或执行特定任务。

简单的说,就是把我们手动的操作封装成函数,先跟大模型报备,然后让大模型调用执行。

如果你有一些Dify,Coze或N8N的经验,那么就会了解这些无代码或低代码的拖拉拽的智能体/工作流的搭建中,也需要除LLM外的一些其他控件,以程序员视角的Function Calling,在它们视角看来就是一个一个的插件。 插件是一个工具集,包含一个或多个工具,而每个工具都是一个可调用的API。

常见插件类别:

  • 📱信息查询:搜索、新闻、天气、地图
  • 📊数据分析:股票、汇率、图表生成
  • 🎨内容创作:图片生成、视频编辑
  • 💼效率工具:邮件、日历、翻译、计算器
  • 🛍️生活服务:美食、旅游、购物推荐

看起来这些插件的功能有大有小,回到我们的智能编程助手上来,我们首先要实现的几个工具并不困难,主要是读文件,写文件,执行终端命令这三个,对于代码只有少部分修改的情况我们再添加一个edit的工具就可以了。

制作第一个工具

以一个查询指定城市气温的工具为例,这个函数没有添加实际网上查询功能,只是返回一个固定的数据,以json的格式返回。

添加一个function calling的工具,一般需要3处更改(第一个工具还需要主循环改造,后续可以不变)

  1. - 工具函数 — get_city_temperature(city) 查询城市温度,返回包含城市、温度、单位的字典
  2. - Schema 定义 — TOOLS 列表声明工具的 name、description 和 parameters(city 为必填字符串)
  3. - 函数映射 — AVAILABLE_FUNCTIONS 字典将工具名映射到实际函数
  4. - 主循环改造 — API 请求增加 tools=TOOLS 参数;检测 tool_calls 后执行函数、将结果以 role: "tool" 追加到消息历史、二次请求获取最终回复;Token 统计适配两次调用的场景

工具函数(1)

CITY_TEMPERATURES = {
    "北京": 25,
    "上海": 28,
}

def get_city_temperature(city: str) -> dict:
    """查询指定城市的当前温度"""
    temp = CITY_TEMPERATURES.get(city)
    if temp is not None:
        return {"city": city, "temperature": temp, "unit": "摄氏度"}
    else:
        return {"city": city, "temperature": None, "unit": "摄氏度", "error": f"未找到城市 {city} 的温度数据"}

Schema定义(2)

# 工具 schema 定义
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_city_temperature",
            "description": "查询指定城市的当前温度",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "要查询温度的城市名称,例如:北京、上海"
                    }
                },
                "required": ["city"]
            }
        }
    }
]

工具函数映射(3)

# 可用工具函数映射
AVAILABLE_FUNCTIONS = {
    "get_city_temperature": get_city_temperature
}

更改消息发送(4) 添加 tools=TOOLS,并且检查是否需要调用工具,这里的TOOLS就是上面Schema定义的内容

    # 发送完整消息历史
    response = client.chat.completions.create(
        model=BASE_MODEL,
        messages=messages,
        tools=TOOLS,
        stream=False,
        extra_body={"enable_thinking": False}
    )

    # 检查是否需要调用工具
    message = response.choices[0].message
    if message.tool_calls:
        # 执行每个工具调用
        for tool_call in message.tool_calls:
            ......

然后运行程序

你: 北京天气?

{
    "content": "",
    "refusal": null,
    "role": "assistant",
    "annotations": null,
    "audio": null,
    "function_call": null,
    "tool_calls": [
        {
            "id": "call_cf01139ba2a347bf82d86cb4",
            "function": {
                "arguments": "{\"city\": \"北京\"}",
                "name": "get_city_temperature"
            },
            "type": "function",
            "index": 0
        }
    ]
}

[工具调用] get_city_temperature({'city': '北京'})
[工具结果] {"city": "北京", "temperature": 25, "unit": "摄氏度"}

AI: 北京当前的温度是25摄氏度。

制作read_file工具

上面已经制作了第一个function calling的工具,接下来我们要通用4个工具来实现入门级的智能编程助手,我们完全可以借鉴Claude Code CLI, OpenCode等它们的function calling的tools的经验,例如在OpenCode中输入:你有哪些function calling的tools

Looking at my tool set, I have these tools available:

  1. bash - Execute shell commands
  2. edit - Edit files (exact string replacements)
  3. glob - File pattern matching
  4. grep - Content search
  5. question - Ask the user questions
  6. read - Read files
  7. skill - Load specialized skills
  8. task - Launch sub-agents
  9. todowrite - Create/maintain task lists
  10. webfetch - Fetch URLs
  11. websearch - Search the web
  12. write - Write files

再次询问:对于其中的read_file工具,TOOLS中是怎么定义的?

以下是 read 工具(对应 read_file)的结构定义:

{
  "name": "read",
  "description": "读取文件内容,支持偏移量和行数限制",
  "parameters": {
    "type": "object",
    "properties": {
      "filePath": {
        "type": "string",
        "description": "要读取的文件绝对路径"
      },
      "offset": {
        "type": "integer",
        "description": "起始行号(从1开始)",
        "minimum": 0
      },
      "limit": {
        "type": "integer",
        "description": "最大读取行数(默认2000)",
        "minimum": 0
      }
    },
    "required": ["filePath"]
  }
}

行为特征:

  • 只传 filePath → 从头读取最多 2000 行
  • 传 offset=100 → 从第 100 行开始读
  • 传 offset=100, limit=50 → 从第 100 行往下读 50 行
  • 返回格式:每行以 行号: 内容 前缀标记
  • 行超长(>2000 字符)会被截断
  • 如果 path 不存在 → 返回错误

在你的实现里,本地 read_file 函数基本只需 open(path).readlines() 配合切片即可等价实现。

有了这些Schema的定义,我们就可以复用AI让他复刻出对应的read_file的python函数了:

根据以下内容,给当前py程序里添加read_file工具,包括函数,Schema,以及AVAILABLE_FUNCTIONS映射:

以下是 read 工具(对应 read_file )的结构定义:

{

"name": "read",

"description": "读取文件内容,支持偏移量和行数限制",
。。。

这里可以稍微注意一下,如果你的文件有中文字符,那么需要告诉AI让它来解决encoding的问题。

AI给你把工具写完后,运行可得:

你: 你好,你有哪些tools

AI: 你好!我目前拥有以下两个工具:

1.  **get_city_temperature**
    *   **功能**:查询指定城市的当前温度。
    *   **参数**:`city`(字符串),例如:北京、上海、广州。

2.  **read**
    *   **功能**:读取文件内容,支持指定起始行号和最大读取行数。
    *   **参数**:
        *   `filePath`(字符串):要读取的文件绝对路径。
        *   `offset`(整数,可选):起始行号(从1开始)。
        *   `limit`(整数,可选):最大读取行数(默认2000)。

如果你需要查询天气或读取文件内容,可以随时告诉我!
----------

你: 读取 token_quota.json文件

{
    "content": "",
    "refusal": null,
    "role": "assistant",
    "annotations": null,
    "audio": null,
    "function_call": null,
    "tool_calls": [
        {
            "id": "call_e75350c280a348ad85f16898",
            "function": {
                "arguments": "{\"filePath\": \"token_quota.json\"}",
                "name": "read"
            },
            "type": "function",
            "index": 0
        }
    ]
}

[工具调用] read({'filePath': 'token_quota.json'})

制作write,Edit, bash工具

有了function calling 的tools的添加的方法,也有了上面读取文件(没有读pdf等其他格式功能)的工具的制作,现在就直接复用AI把后面的3个工具也完成吧。

对于其中的write工具,TOOLS中是怎么定义的?
对于其中的edit工具,TOOLS中是怎么定义的?
对于其中的bash工具,TOOLS中是怎么定义的?另外,我的bash主要是在powershell上使用的,且电脑有很多中文字符,之前写的bash总会中文字符乱码,请统一给我一个合理的说明

完成后进行测试

你: 你好,你有哪些tools

AI: 你好!我拥有以下工具,可以用来协助你完成各种任务:

1.  **get_city_temperature**
    *   **功能**:查询指定城市的当前温度。
    *   **参数**:`city`(城市名称,如北京、上海等)。

2.  **read**
    *   **功能**:读取文件内容,支持指定起始行号和最大读取行数。
    *   **参数**:`filePath`(文件绝对路径),`offset`(起始行号,可选),`limit`(最大行数,可选)。

3.  **write**
    *   **功能**:将内容写入文件(如果文件已存在,则会完全覆盖原有内容)。
    *   **参数**:`filePath`(目标文件绝对路径),`content`(要写入的内容)。

4.  **edit**
    *   **功能**在文件中进行精确的字符串替换,用于修改文件内容。
    *   **参数**:`filePath`(文件绝对路径),`oldString`(要被替换原文),`newString`(替换后的新文本),`replaceAll`(是否替换所有匹配项,可选)。

5.  **bash**
    *   **功能**:在 PowerShell 终端中执行命令,并返回输出结果、错误信息和退出代码。
    *   **参数**:`command`(要执行的命令),`description`(命令简要说明),`timeout`(超时时间,可选),`workdir`(工作目录,可选)。

你可以告诉我你需要做什么,我会选择合适的工具来帮助你!

实现半自动的编程助手

我们运行写好的程序,让它来编写一个python的程序

你: 请写一个bubble_sort.py的程序,包括一个冒泡排序最简单的函数,一个生成随机N个,M位的整数列表函数(默认N=10,M=3),运行时先打印列表,排序后再打印列表

[工具调用] write({'filePath': 'bubble_sort.py', 'content': 'import random\ 。。。

[工具结果] {"filePath": "bubble_sort.py", "success": true, "bytesWritten": 854}

AI: 我已经创建了 `bubble_sort.py` 文件。这个程序包含:

1.  `bubble_sort(arr)`: 一个最简单的冒泡排序实现。
2.  `generate_random_list(N=10, M=3)`: 生成 N 个 M 位随机整数,默认 N=10, M=3。
3.  主程序部分:生成列表,打印原始列表,排序后打印排序后的列表。

你可以运行这个程序来查看效果。如果需要修改或执行它,请告诉我!

你: run it

[工具调用] bash({'command': 'python bubble_sort.py', 'description': '运行冒泡排序程序'})
[工具结果] {"stdout": "原始列表: [982, 537, 743, 765, 433, 220, 404, 752, 519, 746]\n排序后列表: [220, 404, 433, 519, 537, 743, 746, 752, 765, 982]\n", "stderr": "", "exit_code": 0}

AI: 程序运行成功!输出结果如下:

*   **原始列表**: `[982, 537, 743, 765, 433, 220, 404, 752, 519, 746]`
*   **排序后列表**: `[220, 404, 433, 519, 537, 743, 746, 752, 765, 982]`

你: 把冒泡排序的函数进阶到一轮没有交换就退出的版本

[工具调用] read({'filePath': 'bubble_sort.py'})
[工具结果] {"filePath": "bubble_sort.py", "content": "1: import random\n2: \n3: 。。。

AI:

很明显,程序只执行了read_file的工具调用,但没有自动进行Edit,但没有关系,因为上下文都在消息列表中保存着,再把消息列表发一次就能继续了

你: 继续

[工具调用] edit({'filePath': 'bubble_sort.py', 'oldString': 'def bubble_sort(arr): 。。。

[工具结果] {"filePath": "bubble_sort.py", "success": true, "replacements": 1}

AI: 我已经更新了 `bubble_sort.py` 中的 `bubble_sort` 函数。现在的版本增加了一个 `swapped` 标志位,如果在某一轮遍历中没有发生任何交换,说明列表已经有序,循环会提前终止,从而提高效率。

你: 运行这个程序

[工具调用] bash({'command': 'python bubble_sort.py', 'description': '运行优化后的冒泡排序程序'})
[工具结果] {"stdout": "原始列表: [706, 867, 174, 376, 760, 447, 985, 105, 356, 845]\n排序后列表: [105, 174, 356, 376, 447, 706, 760, 845, 867, 985]\n", "stderr": "", "exit_code": 0}

AI: 程序运行成功!输出结果如下:

*   **原始列表**: `[706, 867, 174, 376, 760, 447, 985, 105, 356, 845]`
*   **排序后列表**: `[105, 174, 356, 376, 447, 706, 760, 845, 867, 985]`

优化后的冒泡排序函数正常工作,能够正确地对随机生成的整数列表进行排序。如果在某一轮遍历中没有发生交换,它会提前退出,提高了效率。

小结

这里我们学习了function calling的工具,工具相当于Coze中的插件,它是大模型的延伸,通过调用来解决一些实际的问题。

制作一个工具需要3个部分,1是函数实现,2是Schema的TOOLS,3是工具与函数的映射列表。

实现入门版的智能编程助手,除了能通过API调用大模型外,还需要至少Read,Write,Bash以及Edit这4个工具。

当前的程序每次只能调用一个工具,例如读取代码再更改代码的操作它只会执行前面的读取代码,由于多轮对话的记忆机制,只需要继续/ go ahead就能继续调用后续的工具,因此称它为半自动的编程助手。

我们到后面的skills工具再改进为工具多轮调用,这一节先到这里吧。


 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐