游乐游手机版
首页/AI教程/文章详情

零基础复现Claude Code第四篇:读写文件能力实现

时间:2026-05-29 15:28
开篇:从 "纸上谈兵 "到 "真刀真枪 " 上一篇文章里,咱们已经把 ReAct 循环的骨架搭起来了——Agent 终于会 "思考 "了。它能在输出里看到类似这样的内容: Thought: 我需要读取main pyAction: read_file( main py ) 然而这只是一段文本,文件并没有真的被读取

开篇:从"纸上谈兵"到"真刀真枪"

上一篇文章里,咱们已经把 ReAct 循环的骨架搭起来了——Agent 终于会"思考"了。它能在输出里看到类似这样的内容:

零基础复现Claude Code(四):双手篇——赋予读写文件的能力

Thought: 我需要读取main.pyAction: read_file('main.py')

然而这只是一段文本,文件并没有真的被读取。第 3 篇的成果是实现了完整的 ReAct 循环——模型能思考、能输出 Action、能看到 Observation、能根据结果继续思考。但工具还是模拟的。

这一篇是整个系列真正的实操转折点:从模拟到真实,从理论到实践。

本节目标

读完这篇文章,你将:

  • 理解工具调用的完整闭环——从模型输出到函数执行再到结果反馈的全流程
  • 实现真正的文件读写工具——不再硬编码,而是真的操作文件系统
  • 掌握工具分发器的设计——如何把字符串"read_file('main.py')"转换成真正的函数调用
  • 学会安全地操作文件——避免误删、误改重要文件

原理深潜:工具调用的完整闭环

回到第一篇和第三篇的公式

还记得我们在第一篇建立的公式吗?

循环 t = 0, 1, 2, ...:Thought_t, Action_t = LLM(S_t) ← 第2篇解决了这部分
Observation_t = Execute(Action_t) ← 第3篇实现了循环,本篇实现真实执行
S_{t+1} = S_t + (Thought_t, Action_t, Observation_t)

第 3 篇我们实现了循环框架,但Execute(Action_t)还是硬编码的模拟:

def execute_action(self, action):
if 'read_file' in action:
return "文件内容:..."# 假数据

这一篇我们要实现真正的Execute(Action_t)

def execute_action(self, action):
# 解析action字符串,提取函数名和参数
# 调用真正的read_file函数
# 返回真实的文件内容

工具调用闭环的4个步骤

让我们用图解展示完整的闭环:

PC端完整版:

┌─────────────────────────────────────────────────────────┐
│步骤1:模型输出 │
│LLM → "Thought: ...\nAction: read_file('main.py')"│
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│步骤2:解析Action字符串 │
│"read_file('main.py')" → { │
│tool_name: "read_file",│
│args: ["main.py"]│
│}│
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│步骤3:执行真实工具 │
│调用Python函数:read_file("main.py") │
│→ 打开文件 → 读取内容 → 返回字符串 │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│步骤4:格式化结果,反馈给模型 │
│"Observation: 文件内容:\ndef main():\n..."│
│→ 加入messages列表 → 模型看到结果 → 继续思考 │
└─────────────────────────────────────────────────────────┘

手机端简化版:

模型输出"Action: read_file('main.py')"

解析字符串tool_name="read_file" args=["main.py"]

执行真实函数read_file("main.py")

返回结果"Observation: 文件内容..."

反馈给模型

关键洞察:步骤2(解析字符串)是最容易出错的地方,也是本篇的重点。

两种工具定义方式的对比

在真实的Agent系统中,有两种主流的工具定义方式:

方式A:JSON Schema(OpenAI Function Calling)

tools = [{
"type": "function",
"function": {
"name": "read_file",
"description": "读取文件内容",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "文件路径"}
},
"required": ["path"]
}
}
}]

优点:模型直接输出JSON格式的工具调用,不需要解析字符串
缺点:需要API支持Function Calling

方式B:Python函数签名(我们的简化版)

def read_file(path: str) -> str:
"""读取文件内容"""
with open(path, 'r') as f:
return f.read()

优点:简单直观,不依赖特殊API
缺点:需要解析模型输出的字符串

我们的教学版用方式B,因为它更容易理解"模型输出什么,我们怎么执行"。

动手实操:实现真正的文件读写工具

现在开始写代码。目标很明确:替换第3篇中的硬编码模拟,接入真实的文件操作。

第一步:实现read_file工具

创建一个新文件tools.py

import os

def read_file(path: str) -> str:
"""读取文件内容

参数:
path: 文件路径(相对或绝对路径)

返回:
文件内容(字符串)

异常:
如果文件不存在或无法读取,返回错误信息
"""

try:
# ? 安全检查:确保路径存在
if not os.path.exists(path):
return f"错误:文件不存在 - {path}"
# ? 安全检查:确保是文件而不是目录
if not os.path.isfile(path):
return f"错误:{path} 是一个目录,不是文件"
# ? 读取文件内容
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
# ? 限制返回内容的长度,避免超出Token限制
MAX_LENGTH = 5000 # 约1000个Token
if len(content) > MAX_LENGTH:
return f"{content[:MAX_LENGTH]}\n\n... (文件太长,已截断,共{len(content)}字符)"
return content
except Exception as e:
return f"错误:无法读取文件 - {str(e)}"

代码解读:

  • try-except捕获所有异常,避免程序崩溃
  • 检查文件是否存在、是否是文件(而不是目录)
  • 限制返回内容长度,避免超出模型的Token限制
  • 返回错误信息而不是抛出异常,让Agent能看到错误并调整策略

第二步:实现write_file工具

继续在tools.py中添加:

def write_file(path: str, content: str) -> str:
"""写入文件内容

参数:
path: 文件路径
content: 要写入的内容

返回:
成功或错误信息
"""

try:
# ? 安全检查:确保父目录存在
parent_dir = os.path.dirname(path)
if parent_dir and not os.path.exists(parent_dir):
return f"错误:父目录不存在 - {parent_dir}"
# ? 安全检查:如果文件已存在,先备份
if os.path.exists(path):
backup_path = f"{path}.backup"
with open(path, 'r', encoding='utf-8') as f:
backup_content = f.read()
with open(backup_path, 'w', encoding='utf-8') as f:
f.write(backup_content)
# ? 写入文件
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
return f"成功:文件已保存到 {path}"
except Exception as e:
return f"错误:无法写入文件 - {str(e)}"

代码解读:

  • 检查父目录是否存在(避免写入到不存在的路径)
  • 如果文件已存在,先创建.backup备份(防止误改)
  • with open确保文件正确关闭

第三步:实现工具分发器(核心)

这是本篇最关键的部分——如何把字符串"read_file('main.py')"转换成真正的函数调用?

继续在tools.py中添加:

import re

def parse_action(action: str) -> tuple:
"""解析Action字符串,提取工具名和参数

参数:
action: 字符串,如 "read_file('main.py')" 或 "write_file('test.py', 'content')"

返回:
(tool_name, args) 元组
例如:("read_file", ["main.py"])
"""

# ? 正则表达式解析:工具名(参数1, 参数2, ...)
# 匹配模式:函数名 + 括号 + 参数列表
match = re.match(r'(w+)\((.+)\)', action.strip())
if not match:
return None, []
tool_name = match.group(1)
args_str = match.group(2)

# ? 解析参数列表
# 安全注意:使用ast.literal_eval而不是eval,只能解析字面量,防止代码注入
if args_str.strip():
try:
import ast
# 将参数字符串包装成元组再解析
args = ast.literal_eval(f"({args_str},)")
# 如果只有一个参数,返回的是值本身,需要转成列表
if not isinstance(args, tuple):
args = (args,)
return tool_name, list(args)
except:
return tool_name, []
return tool_name, []

def execute_tool(action: str) -> str:
"""执行工具调用

参数:
action: 字符串,如 "read_file('main.py')"

返回:
工具执行结果(字符串)
"""

# ? 步骤1:解析action字符串
tool_name, args = parse_action(action)
if tool_name is None:
return f"错误:无法解析Action - {action}"

# ? 步骤2:根据工具名分发到对应的函数
if tool_name == "read_file":
if len(args) != 1:
return "错误:read_file需要1个参数(文件路径)"
return read_file(args[0])
elif tool_name == "write_file":
if len(args) != 2:
return "错误:write_file需要2个参数(文件路径, 内容)"
return write_file(args[0], args[1])
else:
return f"错误:未知工具 - {tool_name}"

代码解读:

  • parse_action:用正则表达式提取工具名和参数
  • ast.literal_eval解析参数(安全版,避免代码注入)
  • execute_tool:根据工具名分发到对应的函数
  • 返回错误信息而不是抛出异常

第四步:集成到ReActAgent

现在我们修改第 3 篇的react_agent.py,替换硬编码的execute_action

# 在文件开头导入tools模块
from tools import execute_tool

class ReActAgent:
# ... 其他代码保持不变 ...

def execute_action(self, action):
"""执行Action(现在接入真实工具)

参数:
action: 字符串,如 "read_file('main.py')"

返回:
执行结果(字符串)
"""

# ? 直接调用tools模块的execute_tool
return execute_tool(action)

就这么简单!我们只需要替换一个函数,整个Agent就从"模拟"变成了"真实"。

第五步:测试真实的文件读写

创建一个测试文件test_real_tools.py

from react_agent import ReActAgent
import os

# ? 创建一个测试目录和测试文件
os.makedirs("test_workspace", exist_ok=True)

# 创建一个包含Bug的测试文件
with open("test_workspace/buggy.py", "w") as f:
f.write("""def calculate(a, b):
# Bug: 这里应该是加法,但写成了减法
return a - b

result = calculate(5, 3)
print(f"5 + 3 = {result}")"""
)

# 创建Agent
agent = ReActAgent(max_iterations=10)

# 测试任务:让Agent读取文件并找出Bug
result = agent.run("请读取test_workspace/buggy.py文件,找出里面的Bug并告诉我")
print("\n" + "=" * 60)
print(f"最终结果:{result}")

运行这段代码,你会看到类似这样的输出:

用户:请读取test_workspace/buggy.py文件,找出里面的Bug并告诉我
============================================================
[第 1 轮]
? Thought: 用户想让我读取buggy.py文件并找出Bug,我应该先读取文件内容
? Action: read_file('test_workspace/buggy.py')
? Observation: def calculate(a, b):
# Bug: 这里应该是加法,但写成了减法
return a - b

result = calculate(5, 3)
print(f"5 + 3 = {result}")

[第 2 轮]
? Thought: 我看到了Bug!注释说应该是加法,但代码写的是减法(a - b)。而且print语句也显示"5 + 3",但实际计算的是减法。
✅ Answer: 找到Bug了!在calculate函数中,注释说应该是加法,但代码写的是减法(return a - b)。应该改成 return a + b。这会导致程序输出错误的结果(5 - 3 = 2,而不是5 + 3 = 8)。
============================================================
最终结果:找到Bug了!在calculate函数中,注释说应该是加法,但代码写的是减法(return a - b)。应该改成 return a + b。这会导致程序输出错误的结果(5 - 3 = 2,而不是5 + 3 = 8)。

恭喜!你的Agent现在真的能读取文件了!

⚠️ 安全警告(必读)

安全原则

  1. 在测试目录中运行

    # ✅ 好的做法
    os.chdir("test_workspace") # 切换到测试目录
    agent.run("帮我修Bug")

    # ❌ 危险的做法
    agent.run("帮我修Bug") # 在项目根目录运行,可能误改重要文件

  2. 写入前检查路径

    def write_file(path: str, content: str) -> str:
    # ? 只允许写入test_workspace目录
    if not path.startswith("test_workspace/"):
    return "错误:只能写入test_workspace目录"
    # ... 其他代码

  3. 先用Git备份

    git add .
    git commit -m "备份:测试Agent前的状态"
    # 现在可以放心测试了,出问题就git reset --hard

  4. 限制可执行的命令(下一篇会讲)

    # ❌ 危险:允许任意命令
    run_cmd("rm -rf /")

    # ✅ 安全:只允许只读命令
    ALLOWED_COMMANDS = ["ls", "cat", "grep", "git status"]

真实案例:一个误删文件的故事

有个开发者让Agent"清理临时文件",Agent执行了:

Action: run_cmd('rm -rf temp*')

结果把temp_important_data.json也删了。

教训:

  • 永远不要让Agent执行rm -rf
  • 写入/删除前,先让Agent列出会影响哪些文件
  • 重要文件用Git管理

与真实代码的对照

在真实的Claude Code实现中(rust版本),这部分对应的是:

我们的实现真实代码位置关键差异
read_file()crates/runtime/src/file_ops.rsread_file()真实版支持二进制文件、大文件分块读取
write_file()crates/runtime/src/file_ops.rswrite_file()真实版支持diff模式、权限检查
execute_tool()crates/runtime/src/conversation.rsToolExecutor trait真实版用trait实现,支持动态注册工具
parse_action()不需要,真实版用Function Calling API真实版模型直接输出JSON,不需要解析字符串

想深入研究的读者:

  • 打开crates/runtime/src/file_ops.rs,搜索pub fn read_file,你会看到完整的文件操作逻辑
  • 打开crates/tools/src/lib.rs,可以看到工具注册和分发的机制

为什么我们用字符串解析,真实版用JSON?

方案优点缺点适用场景
字符串解析简单,不依赖特殊API容易出错,难以处理复杂参数教学版、原型
Function Calling健壮,模型直接输出结构化数据需要API支持生产环境

我们的教学版用字符串解析是为了让你看清楚"模型输出什么,我们怎么执行"。真实的Claude Code用OpenAI的Function Calling API或Anthropic的Tool Use API,模型直接输出JSON格式的工具调用。

工具设计的3个原则

通过上面的实现,我们总结出设计Agent工具的3个原则:

原则1:工具应该返回字符串,而不是抛出异常

❌ 不好的设计:

def read_file(path):
with open(path, 'r') as f:
# 文件不存在时抛出异常
return f.read()

✅ 好的设计:

def read_file(path):
try:
with open(path, 'r') as f:
return f.read()
except FileNotFoundError:
return f"错误:文件不存在 - {path}"

为什么? Agent需要看到错误信息才能调整策略。如果抛出异常,循环就中断了。

原则2:工具应该有明确的输入输出格式

❌ 不好的设计:

def process_file(path, mode=None, encoding=None, ...):
# 参数太多,模型容易搞混

✅ 好的设计:

def read_file(path: str) -> str:
"""读取文件内容"""

def write_file(path: str, content: str) -> str:
"""写入文件内容"""

为什么? 参数越简单,模型越不容易出错。

原则3:工具应该有安全边界

❌ 危险的设计:

def run_cmd(cmd):
return subprocess.run(cmd, shell=True, capture_output=True).stdout

✅ 安全的设计:

def run_cmd(cmd):
# 检查命令是否在白名单中
if not is_safe_command(cmd):
return "错误:不允许执行此命令"
# ... 执行命令

为什么? Agent可能会犯错,安全边界能防止灾难性后果。

自检清单(读完本篇请确认)

在进入下一篇之前,请确认你能回答以下问题:

  • 工具调用闭环的4个步骤是什么?
  • 为什么工具应该返回字符串而不是抛出异常?
  • parse_action函数的作用是什么?
  • 为什么需要限制read_file返回内容的长度?
  • 你能说出3个让Agent写文件时的安全注意事项吗?

如果都能回答,恭喜你,Agent的"双手"部分你已经掌握了。下一篇见!

⚠️ 新手容易踩的坑

  1. 坑1:忘记处理文件不存在的情况

    • 后果:Agent执行read_file时程序崩溃
    • 正确做法:用try-except捕获异常,返回错误信息
  2. 坑2:write_file没有创建父目录

    • 后果:写入test/data/file.txt时,如果test/data不存在,会失败
    • 正确做法:检查父目录是否存在,或者用os.makedirs(parent_dir, exist_ok=True)
  3. 坑3:用eval解析参数时没有处理异常

    • 后果:如果模型输出格式错误,eval会抛出异常
    • 正确做法:用try-except包裹eval,或者用ast.literal_eval
  4. 坑4:没有限制文件读取的长度

    • 后果:读取一个10MB的文件,Token预算瞬间耗尽
    • 正确做法:限制返回内容长度,超过则截断

下一步:给Agent装上"终端"

现在你已经学会了:

  • 实现真正的文件读写工具
  • 设计工具分发器(从字符串到函数调用)
  • 处理工具执行中的异常
  • 设置安全边界

但有一个关键能力还没有:

Agent还不能执行命令。

比如,Agent修改了代码后,它不能自己运行pytest验证修改是否正确。它只能"盲改",然后等你手动测试。

下一篇,我们将实现终端工具——让Agent能够:

  1. 执行只读命令(lscatgit status
  2. 看到命令的输出
  3. 根据输出调整策略

这就是Agent从"能改代码"到"能验证代码"的关键一步。

预告一个核心问题:如何防止Agent执行危险命令(如rm -rf)?答案在下一篇揭晓。

系列进度

  • ✅ 第1篇:总览与前置准备——Claude Code到底是什么?
  • ✅ 第2篇:地基篇——让模型开口说话(System Prompt的艺术)
  • ✅ 第3篇:灵魂篇——ReAct循环的骨架
  • ✅ 第4篇:双手篇——赋予读写文件的能力
  • ⏭️ 第5篇:终端篇——赋予执行命令的超能力
  • 第6篇:整合篇——组装Mini Claude Code
  • 第7篇:上下文篇——让Agent看懂整个文件夹
  • 第8篇:反思与展望——我们得到了什么,还缺什么?
来源:https://juejin.cn/post/7631862090698031140
上一篇2023年质量月活动总结撰写方法 AI工具助力轻松完成 下一篇项目投资计划书高质量撰写指南与范文示例
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
GPT Workspace通过GPT-5强化Google Workspace,文档表格邮件创作效率与智能化提升
AI教程 · 2026-05-29

GPT Workspace通过GPT-5强化Google Workspace,文档表格邮件创作效率与智能化提升

GPT Workspace 产品介绍:GPT-5 如何增强 Google Workspace 工作效率 如果你每天都在使用 Google Workspace 进行文档撰写、表格处理、邮件沟通和演示制作,一定深有体会:大量重复性的办公任务耗费了宝贵的时间。现在,GPT Workspace 将 GPT-

AI助手提升年终总结与周报效率的精准营销策略
AI教程 · 2026-05-29

AI助手提升年终总结与周报效率的精准营销策略

适合需求:在信息爆炸的时代,企业所承受的竞争压力几乎覆盖了所有维度,其中营销领域尤为令人困扰。无论是撰写年终总结还是生成周报,精准的营销策略已成为不可或缺的需求——没有谁愿意在庞杂的数据中迷失方向。当我们复盘营销活动时,总会思考:过去哪些数字营销策略真正发挥了效果?哪些内容营销策略有待改进?然而实际

Afri Studio 非洲创意工作室
AI教程 · 2026-05-29

Afri Studio 非洲创意工作室

Afri Studio是什么先来聊聊Afri Studio——它是Afri AI团队推出的一款AI媒体创作工作室,目标很明确:把原本高高在上的智能技术拉下神坛,让普通用户也能轻松生成高质量的文本、图像、音频等内容。换句话说,这是一个面向内容创作者、博主、营销人员、艺术家的“AI工具箱”,帮你高效搞定

Geniea专注Midjourney提示词优化提升创意生成效率
AI教程 · 2026-05-29

Geniea专注Midjourney提示词优化提升创意生成效率

Geniea产品详解:Midjourney提示优化工具Geniea是一款专注于Midjourney提示词优化的智能平台,致力于帮助创作者快速生成高质量且富有创意的提示方案。无论您需要电影镜头、食品摄影还是汽车广告等场景的提示词,只需输入简单指令,系统便会自动输出优化后的提示文本,大幅提升创作效率。提

幼儿园大班毕业典礼方案PPT AI轻松制作精彩回顾
AI教程 · 2026-05-29

幼儿园大班毕业典礼方案PPT AI轻松制作精彩回顾

使用情景 每年毕业季来临之际,幼儿园大班毕业典礼的筹备工作,总是牵动着众多老师、家长和孩子们的心弦。这不仅仅是一场简单的活动,更是孩子们人生中首个重要的成长仪式,标志着他们告别幼儿时光、迈向新阶段的里程碑。对于家长而言,这也是一次充满感怀的“毕业”,意味着一段陪伴旅程的暂时落幕。 如何让这场典礼既温