1. 背景:为什么我们需要 Agent?
- 痛点:传统编程中,简单的重复性工作(如加注释、改格式)浪费了开发者大量时间。
- 思路:既然 LLM(大模型)懂代码,能不能让它自己读取文件、修改文件、再保存?
- 方案:利用ReAct(推理+行动)模式,让 LLM 拥有“双手”(工具),实现全自动闭环。
2. 核心架构:ReAct 是什么?
- ReAct = Reasoning(思考) + Acting(行动)
- 流程:
- 感知:用户下达指令(“给代码加注释”)。
- 思考:LLM 决定第一步要调用
read_file。 - 行动:Python 代码执行读取文件操作。
- 观察:将文件内容返回给 LLM。
- 循环:LLM 看到内容,决定调用
write_file写入新代码。 - 结束:任务完成。
3. 核心代码实现
第一步:定义工具
# Agent 的“手”,用来操作文件 def read_file(file_path): """读取文件内容""" try: with open(file_path, 'r', encoding='utf-8') as f: return f.read() except FileNotFoundError: return "错误:文件未找到" def write_file(file_path, content): """写入文件内容""" with open(file_path, 'w', encoding='utf-8') as f: f.write(content) return "文件写入成功"
3.1.1 结构拆解:从定义到参数
拆解一下read_file的 Schema 结构:
{
"type": "function", // 告诉模型:这是一个可调用的函数,不是一段文本
"function": {
"name": "read_file", // 模型决策后返回这个名字,你的 Python 代码靠它找到对应的函数
"description": "...", // 告诉模型这个工具是干嘛的(最关键!下文细说)
"parameters": { // 定义输入数据的格式
"type": "object",
"properties": { // 具体参数列表
"file_path": {
"type": "string", // 类型约束:必须是字符串
"description": "文件路径" // 参数说明:告诉模型这里该填什么
}
},
"required": ["file_path"] // 必填项:如果不填,工具就无法运行
}
}
}
3.1.2 为什么description是灵魂?
很多初学者容易忽略description字段,甚至直接留空。但实际上,description是决定 Agent 智能程度的关键。
LLM 是基于语义理解的,它不会去读你的 Python 源代码。它判断“现在该不该调用这个工具”的唯一依据,就是description里的文字。
一个优秀的description必须具备两点:
- 明确触发条件:告诉模型在什么情况下使用它。
- 清晰的功能边界:告诉模型用了它会有什么效果。
- ❌糟糕的写法:"description": "写文件"
此时模型可能会困惑——“是覆盖写还是追加写?是写二进制还是文本?如果我把路径填错了怎么办?”模糊的描述会导致模型犹豫不决,甚至乱传参数。
- 优秀的写法:"description": "将内容写入指定文件,用于保存修改后的代码"
模型不仅知道了用途(写文件),还明确了场景(保存修改后的代码)。当用户说“帮我修复这个 Bug”时,模型能迅速通过“保存”这个意图匹配到该工具。
3.1.3 参数描述的精准性
不仅是函数描述,参数的description同样重要。
看write_file的参数定义:"content": {"type": "string", "description": "要写入的内容"}
这告诉模型:这个参数是一个字符串。
- 如果你的描述是
"要写入的字节流",模型可能会尝试把文本转成 bytes,导致你的程序报错。 - 如果你的描述是
"修改后的完整代码",模型就会倾向于把整个文件内容传进来,而不是只传修改的一小行,从而保证了写入逻辑的完整性。
第二步:实现 ReAct 主循环
4. 避坑指南
坑点一:Pydantic 对象与字典的冲突
- 现象:在第二轮循环报错
AttributeError: 'CompletionMessage' object has no attribute 'get'。 - 原因:SDK 底层期望传入字典,但
message是对象,直接append导致类型不匹配。 - 解决方案:修复代码(
model_dump或dict()转换)。代码片段:msg_dict = message.model_dump(exclude_unset=True)
坑点二:上下文丢失
- 原因:忘记把“模型之前的决策”存回
messages。 - 后果:模型在第二轮会“失忆”,不知道自己刚才干了什么。
- 解决方案:强调必须先
append(message),再append(tool_result)。
5. 实战演示
test.py修改前后的对比修改前
修改后