AI Coding Agent 看起来像是在“自主编程”:它会读文件、改代码、运行测试、根据报错继续修复,直到给出一个结果。
但从工程实现上看,它没有那么神秘。一个最小可用的 Coding Agent,核心结构通常就是:
while True:
action = llm(context)
if action is tool_call:
result = execute_tool(action)
context.append(result)
continue
break
真正难的不是这个 while 循环,而是怎么构造上下文、怎么设计工具、怎么把工具结果反馈给大语言模型。也就是说,AI Coding Agent 的核心不是“让程序自己有智能”,而是:
让大语言模型在每一轮都看到足够正确的上下文,并允许它通过工具影响外部环境。
为了把这个机制讲清楚,可以从三个层次看:
- Agent 的控制流:LLM 决定下一步动作,程序负责执行。
- Agent 的能力来源:工具决定它能做什么,上下文决定它知道什么。
- Agent 的工程边界:它不会真的记住一切,只是在每次请求时重新读取上下文。
AI Coding Agent 解决的是什么问题
普通聊天式 AI 编程有一个明显限制:模型只能生成文本,不能自己打开项目目录、读取文件、修改代码或运行测试。
比如你让它修复一个项目里的 bug,如果只把报错贴过去,它只能基于这段报错猜测原因;如果项目里还有多个相关文件、配置文件、测试用例,它没有办法主动查看。
Coding Agent 做的事情,就是给大语言模型接上一组工具:
| 能力 | 没有 Agent 时 | 有 Agent 时 |
|---|---|---|
| 查看项目结构 | 人手动复制目录信息 | 模型请求调用 list_files |
| 读取源码 | 人手动粘贴代码 | 模型请求调用 read_file |
| 修改文件 | 模型生成代码,人手动复制 | 模型请求调用 write_file |
| 验证结果 | 人手动运行命令 | 模型请求调用 execute_bash |
| 根据报错继续修复 | 人把报错再贴回去 | 工具结果自动进入下一轮上下文 |
所以,Agent 不是让大语言模型突然拥有“执行能力”。模型仍然只会输出文本。区别在于,程序把某些结构化文本解释成工具调用,然后把执行结果再塞回上下文。
核心流程:Think、Act、Observe 循环
一个典型 Coding Agent 的执行过程可以拆成四步:
- Think:大语言模型根据当前上下文判断下一步要做什么。
- Act:如果模型选择调用工具,宿主程序执行对应函数。
- Observe:工具执行结果被追加到消息历史里。
- Repeat:带着新上下文再次请求模型。
用图表示就是:
flowchart TD
U[用户提出任务] --> C[构造上下文]
C --> L[请求 LLM]
L --> D{是否调用工具}
D -->|是| T[执行工具]
T --> R[工具结果写入上下文]
R --> L
D -->|否| A[输出最终回答]
这里有个容易被忽略的点:程序本身并不知道任务是否完成。结束条件通常是模型在某一轮没有再返回工具调用,而是返回了一段普通回复。
换句话说,Agent 的“完成判断”也来自大语言模型。
为什么说复杂性在上下文里
大语言模型本身是无状态的。每次请求时,它只看到当前传入的消息、系统提示词、工具定义和工具结果。它不会天然记得上一次发生了什么,除非调用方把历史对话再次传进去。
一次请求通常由这些内容组成:
System Prompt
+ 用户消息
+ 历史 assistant 回复
+ 历史工具调用结果
+ 可用工具 schema
+ 可能被读取的文件内容
+ 规则文件 / spec 文件 / 项目约束
所以,Coding Agent 的效果很大程度上取决于“这一轮给模型看什么”。
同样一个模型,如果上下文里没有相关文件,它只能猜;如果上下文里有完整函数、调用关系、测试输出和修改约束,它就更容易做出合理动作。
flowchart LR
SP[System Prompt] --> CTX[本轮上下文]
H[对话历史] --> CTX
TS[工具 Schema] --> CTX
TR[工具执行结果] --> CTX
FS[文件内容] --> CTX
RULE[规则 / Spec] --> CTX
CTX --> LLM[LLM]
LLM --> OUT[下一步动作或最终回复]
这就是上下文工程(Context Engineering)的核心问题:
- 哪些文件应该被读入?
- 历史对话太长时怎么压缩?
- 项目规则应该放在哪里?
- 工具输出应该返回原始文本,还是结构化信息?
- 什么时候让模型自己探索,什么时候要求它先读 spec?
- 如何避免早期需求被挤出上下文窗口?
Agent 产品之间的差异,很多都体现在这些地方。
重新理解几个常见概念
很多听起来很新的 Agent 概念,落到工程实现上,仍然是在解决同一个问题:让模型在正确的时间看到正确的信息。
| 概念 | 工程本质 | 解决的问题 |
|---|---|---|
| MCP(Model Context Protocol,模型上下文协议) | 标准化工具和外部上下文的接入方式 | 让模型稳定调用文件系统、数据库、浏览器、业务系统等外部能力 |
Rules / .cursorrules | 系统提示词或项目提示词的补充 | 固定编码风格、技术栈选择、项目约束,减少模型乱猜 |
| Spec Coding | 把需求、设计、约束写成可反复读取的文档 | 避免长任务中需求漂移,让 Agent 始终围绕目标行动 |
| Skills | 一组按需加载的提示词、文档和工具 | 不把所有知识一次性塞进上下文,需要时再加载 |
| Memory / Smart Forking | 保存历史信息,检索后重新放进上下文 | 模拟长期记忆,但本质仍是检索后拼接上下文 |
这些机制不会让模型“凭空变聪明”。它们的价值在于降低上下文缺失、上下文污染和上下文漂移带来的错误。
用 Python 实现一个最小 Coding Agent
一个迷你 Coding Agent 至少需要四部分:
- 工具函数:真正访问外部世界。
- 工具描述:告诉模型有哪些工具、参数是什么。
- 系统提示词:约束模型的工作方式。
- 主循环:处理工具调用,把结果追加回上下文。
实现里准备四个工具:
def list_files(path: str = ".") -> str:
"""列出目录文件"""
def read_file(path: str) -> str:
"""读取文件内容"""
def write_file(path: str, content: str) -> str:
"""写入文件"""
def execute_bash(command: str) -> str:
"""执行 shell 命令"""
大语言模型不会直接执行这些函数。它只会返回类似这样的结构化意图:
{
"name": "read_file",
"arguments": {
"path": "buggy_math.py"
}
}
宿主程序拿到这个结构后,才会真正调用 Python 函数。
完整代码
创建 toy_coding_agent.py:
import json
import os
import subprocess
from pathlib import Path
from typing import Any, Callable
from openai import OpenAI
# ============================================================
# 1. 工具函数:Agent 真正能做的事情
# ============================================================
def list_files(path: str = ".") -> str:
"""
列出目录下的文件。
为了避免上下文过大,只返回前 100 个文件。
"""
root_path = Path(path).resolve()
if not root_path.exists():
return f"路径不存在: {path}"
if not root_path.is_dir():
return f"不是目录: {path}"
ignored_dirs = {
".git",
".idea",
".vscode",
"__pycache__",
"node_modules",
".venv",
"venv",
"dist",
"build",
}
files: list[str] = []
for root, dirs, filenames in os.walk(root_path):
dirs[:] = [d for d in dirs if d not in ignored_dirs and not d.startswith(".")]
for filename in filenames:
if filename.startswith("."):
continue
full_path = Path(root) / filename
rel_path = full_path.relative_to(root_path)
files.append(str(rel_path))
if len(files) >= 100:
break
if len(files) >= 100:
break
if not files:
return "(目录为空)"
output = "\n".join(files)
if len(files) >= 100:
output += "\n... (文件数量较多,仅展示前 100 个)"
return output
def read_file(path: str) -> str:
"""
读取文本文件,并加上行号。
行号能帮助模型更准确地描述修改位置。
"""
file_path = Path(path)
if not file_path.exists():
return f"文件不存在: {path}"
if not file_path.is_file():
return f"不是文件: {path}"
try:
content = file_path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return f"无法按 UTF-8 读取文件: {path}"
lines = content.splitlines()
if not lines:
return "(空文件)"
numbered_lines = [
f"{index + 1:4d} | {line}"
for index, line in enumerate(lines)
]
output = "\n".join(numbered_lines)
if len(output) > 8000:
output = output[:8000] + "\n... (文件内容过长,已截断)"
return output
def write_file(path: str, content: str) -> str:
"""
创建或覆盖文件。
简化实现里直接覆盖整个文件,真实产品通常会使用 diff/patch。
"""
file_path = Path(path)
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content, encoding="utf-8")
return f"已写入 {len(content.encode('utf-8'))} 字节到 {path}"
def execute_bash(command: str) -> str:
"""
执行 shell 命令。
只应在受控目录或沙箱里使用,不要在生产机上直接运行未知命令。
"""
try:
completed = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=30,
)
except subprocess.TimeoutExpired:
return "命令执行超时,已终止"
output = ""
if completed.stdout:
output += completed.stdout
if completed.stderr:
output += completed.stderr
if not output.strip():
output = "(无输出)"
if len(output) > 4000:
output = output[:4000] + "\n... (输出过长,已截断)"
return f"退出码: {completed.returncode}\n{output}"
AVAILABLE_FUNCTIONS: dict[str, Callable[..., str]] = {
"list_files": list_files,
"read_file": read_file,
"write_file": write_file,
"execute_bash": execute_bash,
}
# ============================================================
# 2. 工具 Schema:把工具能力描述给模型
# ============================================================
TOOLS_SCHEMA = [
{
"type": "function",
"function": {
"name": "list_files",
"description": "列出项目目录中的文件结构。适合在开始任务时了解项目概况。",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "目录路径,默认为当前目录",
}
},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "读取文本文件内容,返回结果会包含行号。",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "要读取的文件路径",
}
},
"required": ["path"],
},
},
},
{
"type": "function",
"function": {
"name": "write_file",
"description": "创建或覆盖一个文本文件。",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "要写入的文件路径",
},
"content": {
"type": "string",
"description": "要写入文件的完整内容",
},
},
"required": ["path", "content"],
},
},
},
{
"type": "function",
"function": {
"name": "execute_bash",
"description": "执行 shell 命令。适合运行测试、执行脚本、安装依赖或查看命令输出。",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "要执行的 shell 命令",
}
},
"required": ["command"],
},
},
},
]
# ============================================================
# 3. Agent 主循环
# ============================================================
def call_tool(name: str, arguments: dict[str, Any]) -> str:
"""
根据模型返回的工具名和参数执行对应函数。
"""
function = AVAILABLE_FUNCTIONS.get(name)
if function is None:
return f"未知工具: {name}"
try:
return function(**arguments)
except Exception as error:
return f"工具执行失败: {type(error).__name__}: {error}"
def run_agent() -> None:
client = OpenAI()
model = os.getenv("OPENAI_MODEL", "gpt-4.1")
system_prompt = """
你是一个命令行里的 AI 编程助手,目标是帮助用户编写、修改、调试代码。
工作规则:
1. 修改代码前,先查看项目结构或读取相关文件,不要凭空猜测文件内容。
2. 修改文件时,尽量保持改动范围小,不要无关重构。
3. 修改后尽量运行脚本、测试或静态检查来验证结果。
4. 如果工具输出显示失败,需要根据错误继续分析。
5. 如果任务需要用户确认危险操作,例如删除大量文件、执行不可逆命令,应先询问。
6. 回复保持简洁,说明做了什么、验证结果是什么。
""".strip()
messages: list[dict[str, Any]] = [
{
"role": "system",
"content": system_prompt,
}
]
print("Toy Coding Agent 已启动,输入 exit 或 quit 退出。")
while True:
user_input = input("\n用户> ").strip()
if user_input.lower() in {"exit", "quit"}:
print("已退出。")
return
if not user_input:
continue
messages.append(
{
"role": "user",
"content": user_input,
}
)
while True:
response = client.chat.completions.create(
model=model,
messages=messages,
tools=TOOLS_SCHEMA,
)
assistant_message = response.choices[0].message
assistant_message_dict = assistant_message.model_dump(exclude_none=True)
messages.append(assistant_message_dict)
tool_calls = assistant_message.tool_calls
if not tool_calls:
print(f"\n助手>\n{assistant_message.content}")
break
if assistant_message.content:
print(f"\n助手思考>\n{assistant_message.content}")
for tool_call in tool_calls:
function_name = tool_call.function.name
raw_arguments = tool_call.function.arguments or "{}"
try:
function_arguments = json.loads(raw_arguments)
except json.JSONDecodeError:
function_arguments = {}
print(f"\n调用工具> {function_name}({function_arguments})")
result = call_tool(function_name, function_arguments)
print(f"工具结果>\n{result}")
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"name": function_name,
"content": result,
}
)
if __name__ == "__main__":
run_agent()
运行方式
安装依赖:
pip install openai
设置 API Key:
export OPENAI_API_KEY="你的 API Key"
如果使用 OpenAI 兼容服务,通常还可以设置 OPENAI_BASE_URL:
export OPENAI_BASE_URL="https://your-compatible-endpoint/v1"
模型名可以通过环境变量指定:
export OPENAI_MODEL="gpt-4.1"
运行:
python toy_coding_agent.py
示例一:让 Agent 创建并运行脚本
输入任务:
请创建一个 fibonacci.py,计算并打印前 20 个斐波那契数。创建完成后运行它验证结果。
一次可能的执行过程是:
调用工具> list_files({'path': '.'})
工具结果>
toy_coding_agent.py
调用工具> write_file({'path': 'fibonacci.py', 'content': '...'})
工具结果>
已写入 214 字节到 fibonacci.py
调用工具> execute_bash({'command': 'python fibonacci.py'})
工具结果>
退出码: 0
0
1
1
2
3
5
8
13
...
这里发生了三件事:
- Agent 先查看当前目录,避免直接假设项目结构。
- 模型决定创建
fibonacci.py,宿主程序执行写文件工具。 - 模型决定运行脚本,工具输出进入上下文后,模型再给出最终说明。
模型没有直接“创建文件”。它只是返回了 write_file 这个工具调用意图,真正的文件写入由 Python 程序完成。
示例二:让 Agent 修复已有代码
准备一个有 bug 的文件 buggy_math.py:
def add(a, b):
return a - b # 错误:应该是加法
print(f"1 + 1 = {add(1, 1)}")
输入任务:
当前目录下的 buggy_math.py 运行结果不对,请修复它,并运行验证。
合理的工具调用顺序通常是:
sequenceDiagram
participant User as 用户
participant Agent as Agent 主循环
participant LLM as LLM
participant Tool as 工具函数
User->>Agent: 修复 buggy_math.py
Agent->>LLM: 发送上下文和工具 schema
LLM-->>Agent: 调用 read_file
Agent->>Tool: read_file("buggy_math.py")
Tool-->>Agent: 返回带行号的源码
Agent->>LLM: 把源码放回上下文
LLM-->>Agent: 调用 write_file
Agent->>Tool: 覆盖 buggy_math.py
Tool-->>Agent: 返回写入成功
Agent->>LLM: 把写入结果放回上下文
LLM-->>Agent: 调用 execute_bash
Agent->>Tool: python buggy_math.py
Tool-->>Agent: 返回 1 + 1 = 2
Agent->>LLM: 把验证结果放回上下文
LLM-->>Agent: 输出最终说明
修复后的文件应该类似:
def add(a, b):
return a + b
print(f"1 + 1 = {add(1, 1)}")
验证输出:
python buggy_math.py
1 + 1 = 2
这个例子能看出 Agent 的工作方式:读文件、判断问题、写文件、运行命令、根据结果结束。每一步都是模型“选择工具”,程序“执行工具”。
Tool Schema 为什么重要
工具函数本身是 Python 代码,模型看不到函数体。它能看到的是工具描述,也就是 TOOLS_SCHEMA。
例如:
{
"type": "function",
"function": {
"name": "read_file",
"description": "读取文本文件内容,返回结果会包含行号。",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "要读取的文件路径"
}
},
"required": ["path"]
}
}
}
这段 schema 的作用有三个:
- 告诉模型工具名是什么。
- 告诉模型什么时候应该用这个工具。
- 约束模型生成参数的格式。
如果工具描述很含糊,模型就更容易乱用工具;如果参数设计很别扭,模型生成的调用也更容易失败。
MCP 解决的也是类似问题:用标准协议描述工具、资源和提示,让不同 Agent 可以用统一方式接入外部能力。
System Prompt、Rules 和 Spec 的关系
系统提示词负责定义 Agent 的基础行为,例如:
修改代码前先读文件。
修改后尽量运行测试。
不要做无关重构。
危险操作需要确认。
Rules 文件和 Spec 文件可以理解为更具体的上下文锚点。
例如项目根目录放一个 AGENTS.md:
# 项目约束
- 使用 Python 3.11。
- 所有新增函数必须带类型标注。
- 修改业务逻辑后必须运行 pytest。
- 不要修改 public API 的函数签名。
- 代码风格遵循 ruff 默认规则。
如果 Agent 每次处理任务前都会读取这个文件,它就更不容易在长任务中偏离约束。
区别可以这样看:
| 类型 | 适合放什么 | 特点 |
|---|---|---|
| System Prompt | 通用行为规则 | 每次请求都带上,适合短而稳定的原则 |
| Rules | 项目编码规范 | 约束技术栈、风格、目录习惯 |
| Spec | 当前任务需求和设计 | 让长任务有稳定目标 |
| Tool Result | 动态观察结果 | 文件内容、命令输出、测试报错 |
Agent 做得好不好,很大程度上取决于这些信息有没有被正确组织。
为什么不能把整个仓库都丢给模型
很多人使用 Coding Agent 时会有一个误解:只要把项目目录挂上去,模型就理解整个仓库了。
实际上,模型只能看到上下文窗口里的内容。即使 Agent 有权限访问整个目录,也不代表它已经读取了所有文件。读取哪些文件、何时读取、读取后如何压缩,都会影响结果。
常见问题包括:
| 错误用法 | 问题 | 更合适的做法 |
|---|---|---|
| 直接让 Agent 修复复杂业务 bug,但不提供入口、报错和复现步骤 | 模型只能在仓库里盲找 | 给出错误现象、相关接口、复现命令、期望结果 |
| 把大表格或大量日志直接塞进对话 | 上下文浪费严重,模型还可能漏看关键行 | 先用 SQL、Pandas、grep 等工具做聚合和筛选 |
| 让 Agent 一次性完成很大的需求 | 需求容易漂移,改动不可控 | 写 spec,拆成小任务,每步验证 |
| 不运行测试就接受结果 | 模型可能生成看似合理但无法运行的代码 | 要求修改后执行测试、脚本或静态检查 |
| 让 Agent 自由重构 | 容易引入无关改动 | 明确限定修改范围 |
把 Agent 当成一个“会使用工具的上下文处理器”,会比把它当成“全自动程序员”更稳定。
这个迷你实现缺了什么
上面的代码能跑通基本流程,但离成熟产品还有距离。真实 Coding Agent 通常还要处理更多工程细节。
| 能力 | 迷你实现 | 成熟产品常见做法 |
|---|---|---|
| 文件修改 | 直接覆盖整个文件 | 使用 diff/patch,展示变更,支持回滚 |
| 上下文管理 | 简单累加消息 | 历史压缩、摘要、检索、优先级管理 |
| 代码搜索 | 只能列文件和读文件 | 语义检索、符号索引、AST 分析 |
| 命令执行 | 直接执行 shell | 沙箱、权限控制、危险命令拦截 |
| 长任务规划 | 依赖模型临场发挥 | plan/spec/todo 状态持久化 |
| 验证机制 | 让模型自己决定是否运行 | 自动检测测试命令、强制验证、失败重试 |
| 用户交互 | 命令行文本 | diff 审阅、文件树、断点确认、任务队列 |
所以,Agent 的最小形态很简单,产品化很难。难点不在 while True,而在安全、上下文、交互、验证和失败恢复。
使用 Coding Agent 时真正要控制的东西
AI Coding Agent 的能力边界可以用一句话概括:
它只能基于当前上下文做下一步选择,并通过已授权工具影响外部环境。
因此,想让它稳定完成任务,需要重点控制四件事。
1. 控制目标
不要只说“帮我优化一下代码”。更好的任务描述是:
请修复 login.py 中用户密码错误时返回 500 的问题。
已知现象:
- 输入错误密码时接口返回 500。
- 期望返回 401,并带上 {"error": "invalid credentials"}。
限制:
- 不要修改数据库表结构。
- 不要改动注册逻辑。
- 修改后运行 pytest tests/test_login.py。
目标越清楚,模型越不需要猜。
2. 控制上下文
需要告诉 Agent 哪些文件、接口、测试和文档相关。如果有关键设计,写进 spec.md 或 AGENTS.md,不要只放在一轮聊天里。
3. 控制工具权限
execute_bash 很强,也很危险。真实环境里至少要限制:
- 禁止删除根目录、家目录等敏感路径。
- 禁止读取密钥文件。
- 对安装依赖、网络请求、数据库操作做确认。
- 在容器或临时工作区运行命令。
4. 控制验证方式
代码生成不等于任务完成。更可靠的闭环是:
flowchart LR
A[明确需求] --> B[读取相关上下文]
B --> C[生成最小改动]
C --> D[运行测试或脚本]
D --> E{是否通过}
E -->|否| B
E -->|是| F[说明改动和验证结果]
让 Agent 每次改完都验证,能显著减少“看起来对,运行就错”的情况。
小结
AI Coding Agent 的核心并不是神秘的自主意识,而是一个持续循环:
LLM 根据上下文选择动作
程序执行工具
工具结果回到上下文
LLM 再做下一步选择
MCP、Rules、Spec、Skills、Memory 等机制,本质上都围绕上下文展开:让模型在合适的时间看到合适的信息,并用合适的工具完成动作。
真正需要工程师掌握的,也从单纯写代码扩展到了三个方向:
- 把需求和约束写清楚。
- 把上下文组织好。
- 把验证闭环设计好。
理解了这个结构,再使用 Claude Code、Cursor、Cline 或其他 Coding Agent,就更容易判断哪些任务适合交给它,哪些地方必须由人来约束和确认。