ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

300行Python手搓生产级Coding Agent:工具调用与安全设计

300行Python手搓生产级Coding Agent:工具调用与安全设计

1. 为什么我要用300行代码手搓一个Coding Agent

市面上关于Coding Agent的讨论已经很多了,但大部分内容要么停留在概念层面,要么直接甩出一个几万行的开源仓库让你自己啃。我最初也是抱着"找个现成的用"的心态去翻各种项目,结果发现一个尴尬的现实:功能齐全的框架往往抽象层太多,想改一个行为要翻五六个文件;而轻量的demo又太玩具,连基本的文件读写和命令执行都处理不干净。

所以我决定自己动手写一个。目标很明确:300行以内的纯Python,不依赖任何Agent框架,但能真正干活。所谓"生产级",不是说它能替代那些成熟的商业产品,而是指它在设计上考虑了真实使用场景中的关键问题——工具调用的安全性、上下文管理、错误恢复、多轮对话的状态维护。这些东西你在玩具demo里是看不到的,但恰恰是决定一个Agent能不能用的分水岭。

这篇文章适合两类人:一是想理解Coding Agent底层运作机制的开发者,二是有一定Python基础、想自己定制一个轻量Agent但不知道从何下手的人。我会把每一块的设计思路、为什么这么选、实际跑起来会遇到什么问题,全部摊开讲。代码不会贴全文,但关键部分都会给出,你跟着走一遍,最后拿到的是一个能读文件、能写代码、能执行命令、能自我纠错的完整Agent。

先说一下整体架构。一个Coding Agent的核心其实就三件事:让模型能调用工具、让工具的执行结果能回到模型、让这个过程能循环到任务完成。听起来简单,但魔鬼在细节里。比如工具调用的参数怎么校验?模型返回了不合法的JSON怎么办?执行命令超时了怎么处理?上下文太长超出了token限制又该怎么办?这些问题我会在后面的章节里逐个拆解。

我选择纯Python实现,不引入LangChain这类框架,原因有三:第一,框架的抽象会掩盖很多关键细节,你学不到真正的东西;第二,300行的体量完全可控,每一行你都能看懂;第三,没有框架依赖意味着你可以把它嵌入到任何环境里,不需要担心版本冲突。当然,代价是有些轮子要自己造,但这恰恰是学习价值所在。

2. 工具层的设计:Agent的手和脚

2.1 工具抽象:一个函数签名搞定所有

Agent要干活,首先得有工具。我把每个工具定义成一个Python函数,加上一个描述性的schema。核心思路是:工具的输入输出都是结构化的,模型只需要知道工具的名字、参数格式和功能描述。

TOOLS = {} def tool(name, description, parameters): def decorator(func): TOOLS[name] = { "function": func, "description": description, "parameters": parameters } return func return decorator

这个装饰器模式很轻量,但足够用。每个工具注册时带上参数schema,这个schema会直接转成模型能理解的JSON Schema格式。为什么不用类继承?因为对于300行的项目来说,函数加装饰器已经足够清晰,类继承反而增加了理解成本。

我最初设计了五个核心工具:read_file、write_file、list_directory、execute_command、search_code。这五个覆盖了Coding Agent最基础的能力——看代码、改代码、跑代码、找代码。后来实际用下来,发现search_code用得不多,因为大部分时候模型会直接用execute_command调grep,所以最终精简到了四个。

2.2 文件读写:路径安全是第一道防线

文件操作看起来简单,但坑最多。第一个要解决的问题是路径逃逸。如果模型生成了一个../../etc/passwd这样的路径,你的Agent就直接变成了一个安全漏洞。我的做法是强制所有路径都相对于一个工作目录,并且在拼接后做一次规范化检查:

import os WORKSPACE = os.path.abspath("./workspace") def safe_path(relative_path): full = os.path.abspath(os.path.join(WORKSPACE, relative_path)) if not full.startswith(WORKSPACE): raise ValueError(f"Path escape detected: {relative_path}") return full

这个检查必须在每次文件操作前执行,不能只在入口做一次。因为模型可能在多轮对话中逐步构造出一个逃逸路径,单次检查拦不住。

第二个坑是文件编码。我一开始用默认的open(),结果遇到非UTF-8文件直接抛异常,整个Agent就卡住了。后来改成显式指定encoding='utf-8', errors='replace',遇到无法解码的字符用替换符处理,至少不会让流程中断。对于二进制文件,直接拒绝读取并返回提示,让模型知道这个文件它处理不了。

第三个细节是写入时的目录创建。模型经常想写一个src/utils/helper.py,但src/utils/目录不存在。如果直接open(path, 'w')会报错。所以写入前要先os.makedirs(os.path.dirname(path), exist_ok=True)。这个exist_ok=True很关键,不然目录已存在时又会抛异常。

2.3 命令执行:超时、输出截断与危险命令拦截

execute_command是最强大也最危险的工

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进