(二)结尾我留了个尾巴:单次调用对付一问一答还行,稍微复杂点就废了。我当时没当回事,结果第二天自己手痒试了下"查上周缺陷数、跟前一周比趋势"------直接歇菜。这活儿得先查、再算、再总结,一串好几步,一次调用根本兜不住。
所以这篇就是怎么让 agent 自己多步跑起来。我一开始想的就是不就是个 while 套 API 嘛,写出来才知道坑都在后头。
一、先写最土的循环
上来别想那么多,把历史丢回去让它接着说,说完 FINISH 就停:
```python
def run(q):
hist = [sys_prompt, {"role": "user", "content": q}]
while True: # 危险:这玩意儿没退出条件
out = llm(hist)
if "FINISH" in out:
return out
hist.append({"role": "assistant", "content": out})
hist.append({"role": "user", "content": "[Observation] 工具结果"})
```
跑起来,两个问题啪就来了。
二、跑飞:它根本不会自己停
我让它"汇总下上周发版都改了啥"。它先翻 git log,翻完去查 Jira,查完去翻文档,然后折回去又翻 git log 看另一分支......我盯着日志数到 30 多步它还在转,最后手动 Ctrl-C 了。
我当场就懵了。后来想明白一件事:模型没有成本概念。它每步只管"下一步干啥",根本不在乎烧了多少 token、转了多少轮。普通 while 好歹有退出条件,模型的循环没有------你指望它自觉收手,它就一直觉得"再确认一下比较稳"。
取舍很直接:软终止(它说 FINISH)必须有硬上限兜着。硬上限怎么定?
- 固定 12 步最省事,但简单任务浪费、复杂任务不够,否了
- 按 tier 设(small 短、top 长)------我起步用的这个
- 按历史步数 P95 × 1.5 最准,但得先攒数据,跑两周再回校准
我先 tier 起步,等手上有步数分布了再换 P95。既不至于一上来被数据卡死,也不至于定个死数后面跟着遭罪。
三、打转:停是停了,但来回兜圈子
这个比跑飞还烦。有次让它"把测试环境配置同步到预发"。它调了 kubectl apply -f config.yaml,返回 Error: already exists。它说"我反思一下",然后------又调了一次 kubectl apply -f config.yaml。还是 already exists。连着 4 次,每次"反思"完还是同一个动作。我看着日志真笑不出来。
原因很简单:没反思,或者反思来得太晚,它错了也不知道错,接着错。
取舍:反思时机我选"失败触发为主 + 终局强制 + 复杂任务定期体检"。每步都反思成本直接翻倍,大部分步本来就没错,纯浪费,否了;失败才反思 ROI 最高;终局强制是防"忙活半天做完了但答非所问";定期体检(每几步或到里程碑)防它静默跑偏。这里有个经验------如果等"失败后才反思"已经白白烧了前面好几步 token,就把触发提前到"连续两步没进展就反思",进度检测比等失败更早发现跑偏。
四、白反思:最阴的一种
就是上面那个打转,它明明"反思"了,文本里写着"我应该用 get_pod_logs 而不是 describe_pod",结果 action 字段填的还是 describe_pod。文本说改了,手上没改。
这种最坑------你以为它在纠偏,其实原地踏步,还给你一种"我在治理"的幻觉。
取舍:反思必须结构化 + 强制分支,我让它只回 JSON:
```
{"verdict": "retry | change_strategy | escalate", "new_params": {...}}
```
关键在 retry 必须带新参数,绝不允许"用相同参数重试"------参数错了必然再错。这条我当企业级硬约束,比不反思还坑的就是这种假反思。
五、烧钱:反思不是免费的
调试那阵我图省事开了每步反思,一个本来 5 步能完的任务烧了平时三倍的钱。后来才反应过来------反思"什么内容"决定花多少钱,查个参数对错犯不着上顶级模型。
取舍:分层。执行层(工具选对没、参数对没)small 7B 就够;规划层(跑偏没、有更短路径没)才上 top 档。这么分下来 80% 的反思走便宜档,只有真出事才烧 top。
企业级坑:不可逆操作谁来拍板
代码助手一个 git push 把没 review 的代码推上去了,或者误删文件、对外发消息------个人项目你一个人 git 随便推,但团队里模型误操作影响的是别人。我把 git push / 删除 / 外发 / 支付这些在工具定义里打标,循环层统一拦截,轮不到模型"自主决定"。
六、一个能跑的骨架
把上面几个决策点落进同一段代码:
python
```
import requests
API = "https://api.deepseek.com/v1/chat/completions"
KEY = "sk-xxx"
# 硬上限按 tier:简单短、复杂长。设计预期值,跑两周用 P95×1.5 回校准
HARD_LIMIT = {"small": 5, "mid": 12, "top": 20}
TIER = "mid"
def llm(messages, model="deepseek-chat"):
r = requests.post(API, headers={"Authorization": f"Bearer {KEY}"},
json={"model": model, "messages": messages, "temperature": 0.3})
return r.json()["choices"][0]["message"]["content"]
SYS = ("你是研发团队助手。多步任务按 Thought→Action→ActionInput→Observation 循环,"
"完成说 FINISH。禁止自行执行 git push / 删除 / 外发,这些必须停下来等人工确认。")
def reflect(hist):
"""失败/打转时触。执行层检查用便宜模型够。返回结构化决策。"""
p = hist + [{"role": "user", "content":
"检查:工具选对没?参数对没?只回JSON:"
"{\"verdict\":\"retry|change|escalate\",\"new_params\":{}}"}]
return llm(p, model="deepseek-chat")
def run(q):
hist = [{"role": "system", "content": SYS}, {"role": "user", "content": q}]
steps = 0
while steps < HARD_LIMIT[TIER]:
steps += 1
out = llm(hist)
print(f"[step {steps}] {out[:100]}")
if "FINISH" in out:
return out
# 解析 Action 调真实工具,观察结果回灌 hist
# 失败检测:工具返回 error -> reflect()
# 打转检测:连续两步同 Action -> reflect()
refl = reflect(hist)
if "retry" in refl and "new_params" not in refl:
return "【反思无效,转人工】" # retry 不带新参数,当失败处理
hist.append({"role": "assistant", "content": out})
hist.append({"role": "user", "content": "[Observation] 工具结果(接真实调用)"})
return "【超出步数上限,转人工】" # 撞硬上限不无限循环
print(run("查上周缺陷数,跟前一周比,说趋势"))
```
while True 换成了带硬上限的 while,reflect 负责失败/打转时的结构化反思,retry 不带新参数直接转人工------几个点对应上面几个坑。打转和失败的具体检测要去解析 Action,这里用注释标了位置。
七、这章的坑,记一下
- 循环要软终止 + 硬上限,硬上限先按 tier,跑两周用 P95×1.5 回校准,别拍固定值。
- 反思失败触发为主、终局强制必做 ;
retry必须带新参数,禁止原地重放。
- 企业级坑:不可逆操作代码硬控,不交给模型。
- 遗留问题:循环有了,工具还没接。它现在"想干活"却"没手",下一步得给它接工具。
你们做 agent 的时候,循环上限是按固定步数还是按任务复杂度来定?我这儿只是引出问题,实际还是根据业务来决定,我的理念一直是,用最合适的技术能力,做最合适的事情,不为了堆技术而炫技。有得有舍,你换来了高可用,就一定会牺牲成本或者延迟,这账谁都绕不开。你如何选择的呢?