☰
300行代码手搓生产级Coding Agent:纯Python实现与架构解析
2026/10/20 10:13:21 网站建设 项目流程

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

市面上关于Coding Agent的讨论已经很多了,但大部分内容要么停留在概念层面,要么直接甩出一个几万行的框架让人望而生畏。我最初接触这个方向的时候,翻了不少开源项目,发现一个尴尬的现实:功能完整的太重,轻量的又太玩具。真正想搞清楚一个Coding Agent到底是怎么运转的,最有效的办法其实不是读别人的代码,而是自己从零写一遍。

这个项目的目标很明确:用纯Python,不依赖任何Agent框架,写一个能真正干活的生产级Coding Agent。所谓“生产级”,不是说它能替代你日常开发,而是说它的架构是完整的、可扩展的、能处理真实任务的——它能读文件、写代码、执行命令、根据报错自我修正,并且有一个清晰的循环控制逻辑。300行左右是核心逻辑的体量,不含测试和配置文件。

适合谁来参考?如果你已经会写Python,对LLM API调用有基本了解,但一直觉得Agent是个黑盒,那这篇内容就是为你准备的。我会把每一层的设计决策、每一个参数的取舍理由都讲清楚,让你看完之后不仅能复现,还能根据自己的需求改造。整篇内容围绕一个核心问题展开:一个Coding Agent的最小完整闭环到底需要哪些组件,它们之间怎么协作,以及在实际跑起来之后会遇到哪些坑。

2. 整体架构设计与核心思路拆解

2.1 为什么选择“单文件+纯标准库”的方案

在动手之前,我先做了一轮方案选型。市面上的Agent框架大致分两类:一类是LangChain、AutoGPT这种重型框架,功能全但抽象层多,调试的时候一层层往下追很痛苦;另一类是各种Demo级的脚本,几十行代码只能跑个问答,遇到真实任务就歇菜。

我最终选择的是中间路线:单文件、纯Python标准库加一个LLM SDK,核心逻辑控制在300行以内。这个选择的理由有三点。第一,单文件意味着没有隐式的依赖注入和全局状态,所有逻辑都在你眼皮底下,出问题的时候排查路径最短。第二,纯标准库意味着你不需要理解任何框架的“魔法”,os、subprocess、json这些模块的行为是确定的、可预测的。第三,300行的约束倒逼我做减法,每一个组件都必须有明确的职责,不能有冗余的抽象。

注意:这里说的“纯Python”指的是核心逻辑不依赖Agent框架,但调用LLM API仍然需要一个HTTP客户端或官方SDK。我用的是标准库的urllib加json,如果你习惯用requests或官方SDK也完全可以替换,不影响架构。

2.2 Agent的核心循环:感知、决策、执行、反馈

一个Coding Agent的本质是一个循环控制系统。它不断重复四个步骤:感知当前状态(读文件、看报错)、做决策(下一步做什么)、执行动作(写代码、跑命令)、获取反馈(命令输出、测试结果)。这个循环的终止条件是任务完成或达到最大轮次。

我用一个while循环来实现这个控制流,每一轮做三件事:把当前上下文发给LLM,解析LLM返回的动作指令,执行指令并把结果追加到上下文。这里的关键设计是上下文管理——不能无限往上下文里塞历史,否则token会爆炸。我的做法是保留完整的动作-结果对,但对文件内容做截断,只保留最近修改的部分和关键片段。

2.3 工具集的设计:少即是多

一个Coding Agent需要哪些工具?我最初列了十几个,后来砍到四个:读文件、写文件、执行shell命令、列出目录。这四个工具的组合已经能覆盖绝大多数编码任务。读文件让Agent了解现状,写文件让它产出代码,执行命令让它验证结果,列目录让它探索项目结构。

为什么不做更多工具?因为每增加一个工具,LLM的决策空间就大一圈,出错概率也随之上升。实测下来,四个工具的Agent在任务完成率上反而比十几个工具的高,因为LLM不需要在太多选项之间纠结。这个经验在多个项目里都验证过:工具的数量和Agent的可靠性往往成反比。

2.4 安全边界:生产级意味着可控

“生产级”这个词容易被误解。我的定义是:Agent的行为必须是可预测、可中断、可审计的。具体到实现上,我加了三道防线。第一,所有shell命令在执行前都会经过一个白名单过滤,危险命令直接拒绝。第二,每个动作执行前都会打印出来,你可以随时Ctrl+C中断。第三,所有文件修改都会先备份原文件,出错可以回滚。

这三道防线加起来不到30行代码,但它们是区分“玩具”和“工具”的关键。我见过太多Demo直接exec()LLM返回的代码,这在实验环境里无所谓,但一旦接入真实项目就是灾难。

3. 核心模块的详细实现与关键细节

3.1 LLM调用层:如何让模型稳定输出结构化指令

Agent的决策质量取决于LLM的输出是否结构化。如果模型返回一段自然语言,你还得写解析器去猜它的意图,那整个系统就不可靠了。我的做法是在system prompt里严格规定输出格式:模型必须返回一个JSON对象,包含action和params两个字段。

SYSTEM_PROMPT = """你是一个编码助手。每次回复必须是一个JSON对象,格式如下: {"action": "工具名", "params": {"参数名": "参数值"}, "thought": "你的思考"} 可用工具:read_file, write_file, run_command, list_dir """

这里有个实操细节:即使你规定了JSON格式,模型偶尔还是会返回带markdown代码块的JSON。所以解析的时候要先剥离json和标记,再做json.loads。如果解析失败,不要直接崩溃,而是把解析错误作为反馈发回给模型,让它重新输出。这个重试机制我设了最多3次,超过就终止当前任务。

另一个关键点是thought字段。让模型在动作之前先输出思考过程,能显著提升决策质量。这个技巧在多个任务上都验证过,加了thought之后,模型选错工具的概率大概下降了三成。

3.2 工具实现:每个工具都要有明确的输入输出契约

四个工具的实现都不复杂,但每个都有需要注意的细节。

read_file接收路径参数,返回文件内容。这里要做两件事:一是限制读取大小,超过一定字节数就截断并提示;二是处理编码问题,用errors='replace'避免因为个别字符导致整个读取失败。

write_file接收路径和内容,写入文件。写入前先检查父目录是否存在,不存在就创建。写入时先写临时文件再重命名,避免写入过程中断导致文件损坏。这个技巧是从数据库的原子写入借鉴来的,在Agent场景下特别有用,因为Agent可能会在写入过程中被中断。

run_command是最需要小心的工具。我用subprocess.run加timeout参数,防止命令卡死。输出同时捕获stdout和stderr,合并后返回。白名单过滤用简单的关键词匹配,拦截rm -rf、shutdown、format这类危险命令。虽然白名单不能覆盖所有情况,但能挡住大部分误操作。

list_dir返回目录下的文件和子目录列表,过滤掉隐藏文件和常见的忽略目录(如__pycache__、.git)。这个过滤很重要,否则Agent会被大量无关文件干扰。

3.3 上下文管理:在有限token里塞进最多有效信息

上下文管理是Agent能否处理长任务的关键。我的策略是分层管理:系统提示词始终保留,最近N轮的动作-结果对完整保留,更早的历史只保留摘要。

具体实现上,我用一个列表存储对话历史,每轮追加assistant的决策和user的工具执行结果。当历史长度超过阈值时,从最老的非系统消息开始删除,但保留每个文件的最后一次操作记录。这样Agent既知道当前状态,又不会被远古历史淹没。

实操心得:文件内容的截断策略很讲究。我试过按行截断、按字符截断、按语义截断,最后发现最简单的方法往往最有效——只保留文件的前200行和后100行,中间用省略号代替。因为Agent修改代码时通常关注文件头部和尾部,中间部分很少涉及。

3.4 错误处理与自我修正:让Agent从失败中学习

Agent执行命令失败是常态,关键是怎么把失败转化为下一步的正确决策。我的做法是把完整的错误输出(包括stderr)原封不动地返回给模型,并在前面加一句提示:“命令执行失败,请分析错误原因并修正”。

这里有个反直觉的经验:不要帮模型总结错误。我一开始试过用规则提取错误类型再告诉模型,结果发现模型自己读原始错误信息的效果更好。因为很多错误的根因藏在细节里,规则提取反而会丢失信息。让模型自己看完整的traceback,它往往能定位到真正的问题。

如果同一个错误连续出现3次,我会在上下文里插入一个强提示:“你已经连续3次遇到相同错误,请换一种思路”。这个机制能有效防止Agent陷入死循环。

4. 完整实操流程:从零跑通一个真实任务

4.1 环境准备与依赖安装

整个项目只需要Python 3.8以上版本和一个LLM API的访问凭证。我用的API兼容OpenAI的接口格式,所以只需要标准库的urllib就能调用。如果你用其他厂商的API,替换请求地址和认证头即可。

# 创建项目目录 mkdir coding-agent && cd coding-agent # 创建主文件 touch agent.py # 设置API密钥环境变量 export LLM_API_KEY="your-key-here" export LLM_BASE_URL="https://api.example.com/v1"

环境变量这种方式比硬编码安全,也方便在不同环境切换。我习惯把密钥放在.env文件里然后用os.environ读取,但为了保持纯标准库,这里直接用系统环境变量。

4.2 核心代码逐段解析

先看主循环的结构:

def run_agent(task, max_turns=20): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": f"任务:{task}"} ] for turn in range(max_turns): response = call_llm(messages) action = parse_action(response) if action is None: messages.append({"role": "user", "content": "解析失败,请重新输出JSON"}) continue if action["action"] == "finish": print("任务完成") break result = execute_tool(action) messages.append({"role": "assistant", "content": response}) messages.append({"role": "user", "content": f"执行结果:{result}"}) messages = trim_context(messages) return messages

这段代码里最值得说的是trim_context的调用位置。我把它放在每轮追加消息之后,而不是之前,这样能保证当前轮的信息一定在上下文里。max_turns=20是个经验值,大部分编码任务在10轮以内能完成,20轮足够处理复杂任务,同时防止无限循环。

call_llm函数负责发HTTP请求,这里要注意设置合理的超时时间。我设的是60秒,因为有些复杂任务的响应会比较慢。超时后不要直接抛异常,而是返回一个错误信息让Agent决定是否重试。

execute_tool是一个分发函数,根据action字段调用对应的工具函数。每个工具函数都返回字符串结果,这样统一了接口,简化了主循环的逻辑。

4.3 一个真实任务的完整执行记录

我拿一个实际任务来演示:让Agent创建一个Python脚本,读取CSV文件并输出统计信息。

第一轮,Agent调用list_dir查看当前目录,发现只有一个agent.py。第二轮,它决定创建analyze.py,调用write_file写入代码。第三轮,它调用run_command执行python analyze.py,但因为没有CSV文件而报错。第四轮,它读取错误信息后,决定先创建一个示例CSV文件,再次调用write_file。第五轮,重新执行脚本,成功输出统计结果。第六轮,Agent返回finish动作,任务结束。

整个过程6轮,耗时约40秒。这个记录我跑了很多次,流程基本稳定。偶尔Agent会在第三轮直接创建CSV再写脚本,顺序不同但结果一致。

4.4 参数调优:温度、最大token与重试策略

LLM调用的参数对Agent行为影响很大。温度我设的是0.2,因为编码任务需要确定性,太高的温度会让Agent做出奇怪的决策。最大token设的是2000,足够容纳代码和思考过程,又不至于让响应太慢。

重试策略分两层:API层面的重试和解析层面的重试。API调用失败(网络问题、限流)重试2次,间隔1秒。解析失败重试3次,每次把错误信息反馈给模型。这两层重试加起来,Agent的鲁棒性提升很明显。

注意:不要设置太高的重试次数。我试过API重试5次,结果遇到持续限流时整个Agent卡住好几分钟。2次是个平衡点,超过就说明服务端有问题,应该让用户知道而不是默默重试。

5. 常见问题与排查技巧实录

5.1 Agent陷入死循环怎么办

死循环是Agent最常见的故障模式。表现是Agent反复执行同一个动作,或者在不同动作之间来回切换但任务没有进展。我的排查思路是看上下文里最近5轮的动作序列,如果发现有重复模式,就手动干预。

代码层面的防护是加一个动作计数器,同一个动作连续出现超过3次就强制插入提示。但更根本的解决办法是改进system prompt,明确告诉模型“如果某个方法失败两次,必须换一种方法”。这个提示加上之后,死循环的概率大幅下降。

5.2 文件写入乱码或内容截断

这个问题通常出在编码处理上。Windows环境下默认编码可能是GBK,而LLM返回的是UTF-8。我的解决办法是在所有文件操作里显式指定encoding='utf-8'。另外,写入大文件时要检查是否超过了LLM的输出token限制,如果内容被截断,Agent会收到不完整的代码,执行时就会报语法错误。

排查方法是让Agent在写入后立即读取文件并检查行数,如果行数明显偏少就重新写入。这个自检步骤我加在了write_file工具里,作为可选的验证环节。

5.3 API调用超时或返回格式异常

API问题分三类:网络超时、认证失败、返回格式不符合预期。网络超时用重试解决,认证失败要检查环境变量,返回格式异常则需要加强解析器的容错能力。

我遇到过一次API返回了HTML错误页面而不是JSON,解析器直接崩溃。后来在解析前加了一个检查:如果响应不以{开头,就当作错误处理,把原始响应返回给Agent。这样即使API出问题,Agent也能给出有意义的反馈,而不是整个程序挂掉。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
Agent反复执行同一动作上下文缺少进展信息查看最近5轮动作序列插入强制换思路提示
文件内容不完整输出token超限检查文件行数分段写入或增大token限制
命令执行无输出命令卡死或超时检查timeout设置设置合理超时并捕获异常
JSON解析失败模型返回格式错误打印原始响应剥离markdown标记后重试
API返回非JSON服务端错误检查响应前几个字符当作错误反馈给Agent

5.5 几个我踩过的坑

第一个坑是路径处理。Agent有时候会返回相对路径,有时候返回绝对路径,如果不统一处理,文件操作会出错。我的做法是在工具函数入口统一把路径转成绝对路径,基于项目根目录解析。

第二个坑是命令注入。虽然加了白名单,但Agent有时候会构造出白名单内但意图奇怪的命令。比如python -c "import os; os.system('...')"这种。我的应对是限制python -c的使用,只允许执行脚本文件。这个限制牺牲了一点灵活性,但安全性提升很大。

第三个坑是上下文膨胀。一开始我没做截断,跑长任务时token消耗飞快,成本很高。后来加了分层截断,token消耗降了大概六成,任务完成率没有明显下降。

6. 扩展方向与个人经验分享

这个300行的Agent跑通之后,我陆续加了一些扩展。一个是加了简单的记忆功能,把每次任务的关键信息存到本地JSON文件,下次启动时加载,这样Agent能记住项目的一些约定。另一个是加了并行工具调用,让Agent能同时读多个文件,减少轮次。这两个扩展各加了不到50行代码,但实用性提升明显。

如果你想让这个Agent更接近“生产级”,我建议优先考虑三件事:加日志系统,把每一轮的输入输出完整记录下来,方便事后分析;加配置文件,把模型参数、工具白名单这些做成可配置的;加一个简单的Web界面,让非技术用户也能用。这三件事做完,它就能在真实团队里跑起来了。

我个人在实际操作中的体会是,Agent的能力上限不取决于代码量,而取决于你对任务的理解和对失败模式的处理。300行能写出一个能用的Agent,3000行也可能写出一个到处是坑的Agent。关键是把核心循环做扎实,把错误处理做细致,剩下的都是锦上添花。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询