“让 AI 帮我写代码”这个事,两年前我听到时第一反应是“这不就是做个聊天窗口嘛”。但真把自己埋进去做了一个能跑、能改、能自测、能自动修复的编码 Agent 之后,我才明白:聊天只是入口,真正值钱的是把大模型的能力嵌进“发现问题—分析问题—执行修复—验证结果”的完整闭环里。
这篇教程不绕弯子,我会把 Agent 的原理拆开讲清楚,然后带着你从零设计一个能实际干活的编码 Agent:它能搜索项目文件、读取代码、执行 Shell 命令、跑测试,拿到报错后自己调整策略再验证。全程给可复现的 Python 代码,也会讲清楚哪些地方容易翻车、为什么容易翻车。无论你是想给团队做一个内部研发助手,还是单纯好奇 Agent 内部到底是怎么回事,这篇文章都适合花一晚上认真过一遍。
1. AI Agent 到底解决什么问题——先看三个真实工作场景
我在动手写 Agent 之前,踩过最大的误区是把大模型聊天接口包装一下就以为是 Agent。后来我把精力转到“真实研发场景里哪些重复劳动最值得自动化”上,才慢慢摸到门道。下面三个场景是让我真正下定决心写自己的编码 Agent 的导火索。
1.1 场景一:让 Agent 做一次全库代码审查
某次内部项目重构,仓储服务里有一批历史遗留代码,十几个模块文件之间互相引用,谁改了谁很容易出问题。人工拆文件一个个看,一个下午就没了。我让 Agent 做的第一件事很简单:遍历整个 src 目录下所有 .py 文件,逐个发给它,让它按预先写好的 Code Review 标准检查问题——包括未处理异常、魔法数字、重复代码、长函数圈复杂度超标等,然后把问题列表按严重程度排序输出。
这个需求如果只靠单纯问答式大模型,我做不了:要读几十个文件,还要记住每个文件之间的关联。而 Agent 只要有“搜索文件列表”“读取文件内容”“写入报告”这几个工具,就能自己循环处理,批量完成后还把报告整理成 Markdown 文件。第一次跑完整个目录,它挑出的重复代码问题里,有六个是真实存在且我们原先没发现的。从那一刻起我意识到,Agent 的杀手级能力不是“写一段独立代码”,而是“把繁琐的流程自动化地跑完”。
1.2 场景二:跨文件依赖分析
代码重构时,最烦的是“改了一个函数签名,调用方全部要跟着改”。人肉搜索引用关系很容易漏。我试着让 Agent 做跨文件依赖分析:给它一个函数名,它自己搜索 grep 所有文件,找到所有 import 和调用位置,把链路的文件列表汇总出来,并且生成依赖关系表,标出哪些文件是高风险变更点。
这个场景里 Agent 必须连续使用工具多次:先搜索,再逐个读文件,再解析上下文,最后输出综合结论。如果没有 Agent 这种“多次工具调用—观察—再调用”的机制,你很难只靠一次 prompt 就拿到准确的依赖分析结果。跑通之后我把这个能力挂到代码提交前的检查流程里,每次改动前先让 Agent 分析影响面,节省了大量排查时间。
1.3 场景三:从需求到测试用例的闭环
最让我觉得值回票价的,是让 Agent 自己“写代码、跑测试、看失败、修代码”的闭环。我给它的任务很直接:实现一个计算订单折扣的函数,要求支持满减、会员折扣、优惠券叠加,并写 5 个单元测试。Agent 第一步写了一个版本,跑 pytest 后失败了,因为一个边界条件没有考虑清楚。它读取了失败堆栈,自己定位到对应函数,重新生成代码,又跑了一轮测试,直到全绿。
这个闭环是大模型聊天永远做不到的。聊天里你只能反复复制粘贴代码和报错,手动来回好几轮。而 Agent 把整个“试错—反馈—修正”循环自动化了,这才是它效率最高的地方。上面三个场景的背后都有同一个核心机制:Agent 不仅能生成内容,还能操纵环境、获取反馈、调整行动。下一节我就把这个机制拆开来看。
2. AI Agent 的核心原理拆解:大模型之外到底还有啥
很多人以为 Agent 就是“大模型套了层工具”,其实不然。一个能稳定干活的 Agent,是一个非常典型的控制闭环系统。我把它拆成四个部分:模型层、工具层、记忆层、执行控制层。
2.1 四个核心组件:模型、工具、记忆、执行控制
- 模型层:提供推理和决策能力,是 Agent 的“大脑”。它负责理解指令、生成计划、选择动作。
- 工具层:Agent 能触碰外部世界的“手”。编码 Agent 最常用的工具是文件操作、Shell 命令执行、代码搜索、测试框架调用。
- 记忆层:Agent 的“工作记忆”和“长期记忆”。工作记忆是当前对话上下文里携带的历史消息;长期记忆可以是对项目结构的索引、历史提交信息或向量数据库里的知识点。
- 执行控制层:协调以上三者的循环逻辑,决定什么时候调用模型、什么时候执行工具、怎么把工具结果反馈给模型、什么时候终止任务。
这里最容易理解偏差的是“工具层”。模型本身并不会真的“调用”工具,它只是输出一个“想调用某个工具”的结构化意图,真正执行工具的是外部代码。你可以把大模型看作一个高级调度员,调度员不会亲自去搬箱子,他只负责下发正确指令,执行层负责真正搬运。
2.2 Agent 循环:推理—行动—观察
编码 Agent 的基本工作方式是一组循环操作。我用文字描述整个流程,比流程图更容易看懂:
- 把任务指令加上历史上下文送给模型。
- 模型判断下一步动作:要么直接生成答案结束任务,要么决定调用某个工具。
- 如果调用工具,系统执行对应的工具函数,把执行结果(比如文件内容、命令输出、报错信息)追加为一条“工具消息”返回给模型。
- 模型看到工具结果后继续推理,决定下一步。
- 重复这个过程,直到模型认为任务完成,或者达到最大循环次数。
这个机制在业界常被叫做 ReAct(Reasoning + Acting)模式,核心就是让模型在“思考”和“行动”之间交替进行。向我这样设计过的项目组里,这个循环就像是给模型装了一个“试错回路”,它不需要每一步都正确,因为它能从工具反馈里学习并修正。
2.3 Function Calling 的底层逻辑
要支撑上述循环,模型得能输出结构化的工具调用指令。目前主流方案是 Function Calling(或在某些框架里叫 Tool Calling)。它的原理并不神秘:系统把你定义好的工具列表——包括工具名称、描述、参数 JSON Schema——连同对话消息一起发给模型。模型的训练让它具备“根据上下文输出符合 schema 的结构化工具调用”的能力。
所以工具定义的“描述质量”,会直接决定 Agent 是否能正确选工具。我把描述写得太笼统,比如“执行命令”,Agent 就在该用文件读取的地方去跑 ls;后来我把描述改成“执行 Shell 命令,适用于运行 Python 脚本、编译、测试、文件操作等场景,传入完整命令字符串”,选工具的准确率立刻上了一个台阶。
3. 环境搭建与工具选型——动手前的准备
逻辑讲通了,接下来就是动手。我把整套开发环境、依赖、工程目录一次说清,你跟着走不会乱。
3.1 语言选型:为什么是 Python
Python 是做 Agent 原型最快路径,原因很直白:它和大模型工具链的配合最成熟,写胶水代码成本低,数据类、JSON 处理、subprocess 调用都顺手。你要是想做成生产级服务,后面还可以用 Java 或 Go 重构执行层,但 Python 做研究和迭代的速度优势无法替代。
实际操作里,Python 版本建议 3.10 以上,因为我会用到一些类型注解和内置特性。依赖方面,核心只需要两个包:一个用于调用大模型接口的 SDK,另一个是 python-dotenv 用于管理环境变量。对,你可能还需要 requests,但是一般 SDK 会自带。
3.2 模型接入方式:云端 API 还是本地部署
我在项目初期做了一个对比,结论直接影响后续成本结构。先说结论:定义型实验阶段,云端 API 起步最快;如果在数据敏感、完全离线要求很高的生产环境里,就用本地部署。两者对比如下:
| 维度 | 云端 API 接入 | 本地部署 |
|---|---|---|
| 起步成本 | 低,注册拿 Key 就能用 | 高,需要 GPU 和模型权重 |
| 部署难度 | 几乎为零 | 需要掌握模型推理框架和显存管理 |
| 数据隐私 | 数据出内网,需要评估合规风险 | 数据完全不出内网 |
| 延迟 | 稳定在线,网络有波动 | 内网低延迟,但算力有限时排队 |
| 长期成本 | 按 Token 付费,高频调用累加明显 | 一次性硬件投入,边际成本低 |
我当时在内部项目里选择了云端兼容接口,因为开发阶段需要频繁调整 prompt 和工具 schema,切换部署成本低。你只需要把 API Key 和 Base URL 配好,代码对所有兼容 chat/completions 接口的模型都可以通用。
3.3 工程目录结构与基础配置
我建议你建立一个清晰的目录,后续扩展多 Agent 或加 RAG 都不会乱:
coding-agent/ ├── .env # API 密钥、模型名、环境配置 ├── requirements.txt # 依赖清单 ├── agent/ │ ├── __init__.py │ ├── core.py # Agent 主循环 │ ├── tools.py # 内置工具定义 │ └── prompts.py # 系统提示词 ├── workspace/ # Agent 实际操作的工作目录 └── tests/ # 自定义测试脚本.env 配置大概长这样:
LLM_API_KEY=你的密钥 LLM_BASE_URL=https://你的接口地址/v1 LLM_MODEL=你的模型标识这种结构的好处是“执行工作区”和“Agent 代码”隔离。Agent 只允许在 workspace 目录里读写文件,避免误操作到 Agent 自身代码目录,这是安全上的第一道防线。
4. 核心代码实现:写一个能“干活”的编码 Agent
原理说完了,下面开始写代码。我会从工具定义到主循环逐步实现,你一边敲一边理解,做完就能跑。
4.1 第一步:定义 Tool 基类与内置工具集
Agent 的工具集建议用一个 Tool 类统一描述,类里面保存工具的元信息(名称、描述、参数 Schema)和实际执行函数。这样主循环只需要遍历 tools 列表,把定义发给模型,解析到调用时再按名字索引执行即可。我把最常用的四个工具列出来:
- search_files:递归搜索文件名/目录名,用通配符匹配。
- read_file:读取指定文件内容,可带行号范围。
- write_file:写入/覆盖文件内容。
- run_command:执行 Shell 命令,捕获 stdout 和 stderr。
对应的 Python 实现我写在这里,你可以直接抄走改成自己的:
# agent/tools.py import os import subprocess from dataclasses import dataclass from typing import Callable, Any def search_files(root: str = ".", pattern: str = "*.py") -> str: """简易文件搜索:返回匹配文件列表""" matches = [] for dirpath, _, filenames in os.walk(root): for f in filenames: if f.endswith(pattern.replace("*", "")): matches.append(os.path.join(dirpath, f)) return "\n".join(matches[:200]) or "无匹配文件" def read_file(path: str, max_chars: int = 5000) -> str: """读取文件并截断,避免大文件撑爆上下文""" if not os.path.exists(path): return f"错误:文件 {path} 不存在" with open(path, "r", encoding="utf-8") as f: return f.read()[:max_chars] def write_file(path: str, content: str) -> str: """写入文件,自动创建父目录""" os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w", encoding="utf-8") as f: f.write(content) return f"文件 {path} 写入成功,共 {len(content)} 字符" def run_command(command: str, timeout: int = 30) -> str: """执行 Shell 命令,返回输出或报错""" try: proc = subprocess.run( command, shell=True, text=True, capture_output=True, timeout=timeout ) result = proc.stdout if proc.stderr: result += "\n[stderr]\n" + proc.stderr return result[-3000:] or "(无输出)" except subprocess.TimeoutExpired: return f"错误:命令执行超时(>{timeout}s)" except Exception as e: return f"错误:命令执行失败 {e}" class Tool: def __init__(self, name: str, description: str, parameters: dict, func: Callable[..., Any]): self.name = name self.description = description self.parameters = parameters self.func = func def to_definition(self) -> dict: return { "type": "function", "function": { "name": self.name, "description": self.description, "parameters": self.parameters, }, } def execute(self, **kwargs) -> str: try: return str(self.func(**kwargs)) except TypeError as e: return f"参数错误:{e},请检查参数名与类型" def get_default_tools() -> list[Tool]: return [ Tool( name="search_files", description="在项目目录中按文件名后缀搜索文件,返回文件列表。适合先了解项目结构。", parameters={ "type": "object", "properties": { "root": {"type": "string", "description": "搜索根目录,默认 workspace"}, "pattern": {"type": "string", "description": "文件名模式,如 *.py"}, }, "required": ["pattern"], }, func=search_files, ), Tool( name="read_file", description="读取文件内容,每次截断到 max_chars 字符以内,适合查看源码或配置。", parameters={ "type": "object", "properties": { "path": {"type": "string", "description": "文件路径"}, "max_chars": {"type": "integer", "description": "最大读取字符数"}, }, "required": ["path"], }, func=read_file, ), Tool( name="write_file", description="写入一个新的代码文件或覆盖现有文件,会创建父目录。", parameters={ "type": "object", "properties": { "path": {"type": "string", "description": "文件路径"}, "content": {"type": "string", "description": "文件完整内容"}, }, "required": ["path", "content"], }, func=write_file, ), Tool( name="run_command", description="执行 Shell 命令,支持运行 Python、pytest、pip 安装等。传入完整命令字符串。", parameters={ "type": "object", "properties": { "command": {"type": "string", "description": "要执行的命令"}, "timeout": {"type": "integer", "description": "超时秒数,默认30"}, }, "required": ["command"], }, func=run_command, ), ]4.2 第二步:实现 Agent 主循环
主循环是整个 Agent 最核心的部分。我把它设计得尽量简洁,把不同关注点分开:发送消息、解析工具调用、执行工具、追加上下文。这里有一个值得注意的地方:工具调用的结果必须严格按照接口协议返回,确保工具执行完成后能正确把结果“喂”回模型,否则模型会陷入混乱。
# agent/core.py import os import json from openai import OpenAI from dotenv import load_dotenv from agent.tools import get_default_tools, Tool load_dotenv() client = OpenAI( api_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL"), ) MODEL_NAME = os.getenv("LLM_MODEL") SYSTEM_PROMPT = ( "你是一个编码助手 Agent,可以调用工具完成代码编写、文件读取、命令执行等任务。" "你的工作目录是 workspace/。必须通过工具操作文件,不要假装执行。" "如果命令执行失败,阅读报错并根据反馈修改代码,再重新验证。" ) def run_agent(user_request: str, max_iterations: int = 20) -> str: tools: list[Tool] = get_default_tools() tool_defs = [t.to_definition() for t in tools] messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_request}, ] for step in range(max_iterations): print(f"\n===== 第 {step + 1} 轮推理 =====") response = client.chat.completions.create( model=MODEL_NAME, messages=messages, tools=tool_defs, tool_choice="auto", ) msg = response.choices[0].message messages.append(msg) # 如果模型没有发起工具调用,说明任务结束 if not msg.tool_calls: return msg.content or "(模型无输出)" print(f"模型发起 {len(msg.tool_calls)} 个工具调用") tool_map = {t.name: t for t in tools} for call in msg.tool_calls: fn_name = call.function.name fn_args = json.loads(call.function.arguments or "{}") print(f" 工具: {fn_name} 参数: {fn_args}") tool = tool_map.get(fn_name) result = tool.execute(**fn_args) if tool else f"未知工具: {fn_name}" # 关键:把工具结果和调用ID配对回传 messages.append({ "role": "tool", "tool_call_id": call.id, "content": result, }) return "已达到最大迭代次数,任务未完成,请调整提示词或增加次数。"这里我要特别强调两个小细节,都是实际调试时最容易卡住的地方。第一,tool_choice必须设成"auto",否则模型可能不会自动发起工具调用。第二,工具回传消息里的tool_call_id必须与模型返回的调用 ID 严格一致,否则接口会直接报错。我早期经常踩这个坑,后来我干脆封装了一个工具类帮助记录。
4.3 第三步:实现“生成代码→执行测试→自动修复”的闭环
能循环调用工具之后,就有意思了。我增加两个高级工具:一是独立的 Python 脚本执行入口,二是 pytest 测试运行器。这样 Agent 就能自己写代码、自己跑测试、自己看结果、自己改代码。我把执行 Python 脚本的逻辑封装成独立的工具函数,方便在沙箱里控制超时和资源。
# agent/python_tools.py import subprocess import tempfile import os def run_python_script(script: str, timeout: int = 15) -> str: """在临时文件里运行一段 Python 代码,避免污染工作区""" with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as f: f.write(script) tmp_path = f.name try: proc = subprocess.run( ["python", tmp_path], text=True, capture_output=True, timeout=timeout ) output = proc.stdout if proc.stderr: output += "\n[stderr]\n" + proc.stderr return output[-3000:] except subprocess.TimeoutExpired: return "错误:脚本运行超时" finally: os.unlink(tmp_path) def run_pytest(path: str = "tests", timeout: int = 120) -> str: """运行指定路径下的 pytest 测试""" proc = subprocess.run( ["python", "-m", "pytest", path, "-x", "-q"], text=True, capture_output=True, timeout=timeout ) output = proc.stdout if proc.stderr: output += "\n[stderr]\n" + proc.stderr return output[-3000:]把这两个工具塞进get_default_tools(),得到一个带执行和验证能力的“手”,剩下就让模型自己发挥。我用下面的用户请求跑过一轮:
“请实现一个 calculate_discount(price, is_member, coupon) 函数,放在 workspace/order.py。要求:会员额外 9.5 折,coupon 是满减金额,折扣不能叠加使最终价格低于原价的 60%。写 5 个测试用例放在 workspace/test_order.py,然后运行 pytest 并修复所有错误,直到全部通过。”
Agent 的决策路径大致是这样的:
- 先 write_file 写出 order.py。
- 再 write_file 写出 test_order.py。
- run_command 跑 pytest。
- 报错,失败断言显示会员折扣逻辑算错了。
- read_file 读回 order.py。
- write_file 修改 discount 计算逻辑。
- 再跑 pytest,直到全绿。
整个过程里 Agent 不需要我插手,唯一的“人工”工作就是起任务和等结果。这个闭环的可靠性取决于两个关键设计:系统提示词里明确写了“必须通过工具操作文件,不要假装执行”,以及工具会返回真实 stderr 内容。这两个设计让 Agent 不能“嘴硬”说自己成功了,它必须面对真实执行结果。
4.4 关键流程复盘:一次典型任务的状态变化
我把一次完整任务的工具调用序列整理成下表,方便你理解一个“简单”任务背后到底发生了多少轮交互:
| 步骤 | 工具调用 | 观察结果 | 决策 |
|---|---|---|---|
| 1 | 无 | 收到用户需求 | 决定创建代码文件 |
| 2 | write_file | 写入成功 | 决定创建测试文件 |
| 3 | write_file | 写入成功 | 决定运行测试 |
| 4 | run_command | 测试失败,返回断言错误 | 决定读取源码 |
| 5 | read_file | 看到错误逻辑 | 决定修复代码 |
| 6 | write_file | 覆盖成功 | 决定重新测试 |
| 7 | run_command | 测试通过 | 输出总结,结束任务 |
这张表反映出 Agent 的本质:每一步行动都被真实环境验证,每一次验证都影响下一步决策。这也是它比“一次性生成代码”靠谱的核心原因。
5. 上下文管理与能力增强——让 Agent 记住该记住的
Agent 跑起来只是第一步,真正让它可用的关键在上下文管理。大模型的上下文窗口再大也有天花板,而编码 Agent 经常要读多个文件、看多次报错、改多轮代码,上下文很容易就被塞满了。
5.1 简单粗暴法:窗口裁剪与历史压缩
我在项目早期用的策略很直白:工具返回内容做截断。比如 read_file 默认只读前 5000 字符,run_command 只保留最后 3000 字符。这样能避免大文件、长日志瞬间占满上下文。但截断的代价是信息丢失,Agent 遇到一个长函数的 Bug,只读前 5000 字符可能根本看不到出错位置。
后来我又增加了“分段读文件”能力:read_file 支持传入行号范围,Agent 可以根据报错堆栈里的行号,只读对应片段。这个改进效果非常明显,上下文占用大幅下降,且命中率更高。你可以把行号范围作为可选参数加进 Tool 的 schema 里,让模型决定用不用。
上下文压缩还有一招:当对话历史超过一定轮数后,让模型先输出一个“阶段性小结”,把已有的关键决策和文件状态浓缩成几百字,然后清空不必要的历史。这个 summary 技巧我用下来效果稳定,代价是要多一次模型调用,但可以保住 Agent 不乱“失忆”。
5.2 项目知识检索:RAG 辅助 Agent
如果 Agent 面对的是一个大型代码库,比如几万个文件,每次都 search_files 再逐文件 read_file 太慢了,而且上下文也不够。这时候适合引入一个轻量级的 RAG(检索增强生成)层。我先离线把代码文件分块、做 embedding,存到向量数据库里;Agent 在需要定位相关代码时,不是盲目搜文件,而是先向量检索相关片段,再按需读取完整文件。
我当时的实现没用什么重型框架,就一个本地向量库加一个简单的 embedding 接口。每次查询时,把用户任务里的关键词做向量检索,返回最相关的 5-8 个代码片段,连同路径信息一并放入上下文。这个增强让小仓库里原本要十几轮搜索的任务,压缩到三四轮就能锁定目标。
不过要注意,RAG 适合“知识召回”场景,不适合“流程执行”场景。代码 Agent 的核心能力还是要落在工具调用和闭环验证上,RAG 只是帮它更快找到下手的第一块石头。
5.3 多 Agent 协作思路与角色分工
单 Agent 做太复杂的任务容易在上下文里打架:既要规划,又要写代码,又要执行测试,还得 review 代码质量。这种情况下我建议拆成多 Agent 协作架构。常见拆法:
- Planner Agent:负责把大任务拆成可执行的小步骤,维护一个任务清单。
- Coder Agent:负责实际代码生成和修改。
- Reviewer Agent:负责审查代码质量,发现潜在问题。
- Tester Agent:负责跑测试并定位失败原因。
它们之间通过一个共享任务数组做协调。Planner 写完任务清单后,Coder 从清单里认领任务;Coder 完成后,Reviewer 检查代码并把问题丢回给 Coder 修改;最后 Tester 跑全量测试并把结果同步给 Planner。
这个架构明显比单 Agent 更抗造,代价是你要写更多协调逻辑。我实践中发现,哪怕只是“Planner + Coder + Tester”三个角色,任务完成率和结果稳定性就已经远超单 Agent。角色提示词里要把职责边界写清楚,比如“你不写代码,你只拆分任务和验收结果”,否则模型会越界。
6. 实战踩坑与调优——我把 Agent 从“玩具”调成了“生产力工具”
做完原型之后,真正浪费我最多时间的不是写功能,而是一堆看起来很诡异但反复出现的问题。我把最有代表性的几个问题整理成排查表,希望帮你少走弯路。
6.1 常见问题快速排查表
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 模型不调用工具,直接编答案 | 工具描述不清晰,或 system prompt 没强调必须用工具 | 强化 prompt:“必须通过工具操作文件,不要假装执行” |
| 调用了工具但参数是空的 | 参数 schema 里的 required 没写全 | 检查 JSON Schema,确保必填字段都标记 required |
| 工具调用 ID 不匹配 | messages 里 tool_call_id 填错或漏填 | 严格按照模型返回的 tool_call.id 回传 |
| 上下文被塞爆 | 工具返回内容太长,历史消息过多 | 做截断、分段读取、阶段性小结 |
| 测试反复失败却修不对 | 报错信息被截断没看到关键错误 | 查看 stderr 部分,扩大日志截断范围 |
| Agent 卡在循环里不结束 | 没有设置最大迭代次数或任务描述不清 | 设置 max_iterations,明确“成功标准” |
| Shell 命令权限太危险 | run_command 无限制,可能误删文件 | 限制工作目录、加入危险命令校验 |
上面每个问题我在开发阶段都遇到过。比如“工具调用 ID 不匹配”这个问题,看起来毫无头绪,其实是我把消息列表里用户消息和工具回传的顺序摆错了。你只要记住一条:先追加模型返回的 assistant 消息,再逐个追加 tool 回传消息,顺序不要乱。
6.2 提示词策略与迭代记录
系统提示词是整个 Agent 最便宜也最有效的“调优旋钮”。我的第一版提示词简单粗暴:“你是编码助手。”结果模型经常假装执行。我迭代了几轮,把以下关键约束补了进去:
- 明确工作目录,避免模型去系统目录翻文件。
- 明确工具使用原则:“能调用工具就不要凭记忆写内容。”
- 明确验证原则:“命令执行失败后必须阅读错误信息并修复,不得忽略。”
- 明确退出条件:“只有测试全部通过后才能输出最终总结。”
这些听起来像废话,但对模型行为影响极大。我建议你测试时把 system prompt 当作和代码同等重要的交付物,每次修改 prompt 都记录 diff 和对任务完成率的影响。不要小看这一行两行的差异,实践中它就是“玩具 Agent”和“生产力 Agent”的分水岭。
6.3 安全边界:防止 Agent 乱来
Agent 能执行 Shell 命令以后,安全问题必须认真对待。我在内部工具里做了三道防护,你可以参考:
- 工作目录隔离。所有 Agent 文件操作都强制限制在 workspace/ 目录内,绝对路径穿越一律拒绝。实现上,write_file 和 read_file 里判断
os.path.abspath(path)是否以 workspace 路径开头。 - 命令黑名单。run_command 里对 rm -rf、mkfs、dd、shutdown 等危险命令做前缀匹配拦截。
- 超时与并发限制。所有命令统一加 timeout,默认 30 秒;同一时间只允许一个 Agent 任务实例运行,避免资源冲突。
有人会问,自己本地实验也要这么严格吗?我的看法是:养成习惯比临时补救省心。因为 Agent 模型不可控,一旦你某天把工作目录指错了,一个 rm -rf 就能让你后悔很久。
6.4 从编码 Agent 到研发效能助手:扩展方向
功能稳定后,我往两个方向做了扩展。一个方向是接入版本控制流程,把 Agent 挂在提交代码前的检查环节,自动触发基于变更文件的代码审查和单测生成。另一个方向是做成内部 Web 服务,团队成员可以在网页上提交需求,后端自动分配一个 Agent 实例执行任务,完成后把结果和日志保存到报告中心。
如果要从头规划这个方向,我建议按这样的优先级走:先做好单 Agent 刷单个任务的能力,再做多 Agent 协作,最后再接入工程化平台。千万不要一上来就铺大平台,否则排查问题时会同时面临“模型问题、工具问题、平台问题”三重困难,定位会非常被动。
最后说一个经常被忽略但很重要的点:Agent 的输出结构和日志记录。我每一轮工具调用都会落盘保存,包括时间、工具名、参数、返回结果。这是后面复现问题、做自动评估的数据基础。没有这套日志,你的 Agent 一旦出错,就只能靠肉眼猜,调试成本极其可怕。
我个人现在最常用的调试方式,是把一次完整任务的所有消息记录导出,逐轮回看工具调用序列,观察模型是在哪一步决策失误的。每次这样回溯,都会发现新的 prompt 优化或者工具设计改进方向。这套方法也是我从“感觉 Agent 好用”到“知道 Agent 为什么好用”的关键一步。如果你也正在做 Agent,建议从第一版代码开始就把日志留下来,它会成为你最有价值的开发资源之一。