忘掉 "agent 是啥" —— 直接看代码。我们从你已经会写的 单次 LLM 调用 开始,一版一版加东西,最后就是一个能收敛的 agent loop。每段代码都能直接抄。
整个例子做同一件事:给定一个任务描述,让 agent 去改一个文件,直到 pytest 通过。 我们不真调 LLM 也不真跑 pytest —— 用一段 假的 LLM 和 假的 shell,你按"Run"就能看到每一版的行为差别。
你已经会写这样的东西:
def call_llm(prompt: str) -> str:
# OpenAI / Anthropic / whatever
return client.chat.completions.create(
model="gpt-5",
messages=[{"role": "user", "content": prompt}],
).choices[0].message.content
# Harness = 你为这一次调用准备的所有东西
answer = call_llm("修好 buggy.py,让 pytest 通过。文件内容:\n" + open("buggy.py").read())
print(answer)
凭直觉把它包一层 while:
def agent_v1(goal: str):
history = []
while True: # ← 没有终止条件
prompt = f"目标: {goal}\n历史:\n" + "\n".join(history)
action = call_llm(prompt) # policy: 模型输出下一步动作
observation = run_shell(action) # env: 真的去执行
history.append(f"> {action}\n{observation}")
if "done" in action.lower(): # ← 让模型自己说"我好了"
break
return history
关键规则:让 policy 决定"下一步做什么",让 verifier 决定"是不是完成了"。 这两个角色永远不要是同一个。
def verify() -> bool:
# 用可执行信号判断,不是问模型
result = subprocess.run(["pytest", "-x"], capture_output=True)
return result.returncode == 0
def agent_v2(goal: str):
history = []
while True:
prompt = f"目标: {goal}\n历史:\n" + "\n".join(history)
action = call_llm(prompt)
observation = run_shell(action)
history.append(f"> {action}\n{observation}")
if verify(): # ← 外部信号,不问模型
return "success", history
if "done" in action.lower(): break
三个终止器一起上,缺一不可:
MAX_STEPS = 10
STALL_LIMIT = 3
def agent_v3(goal: str):
history = []
stall = 0
prev_obs = None
for step in range(MAX_STEPS): # ① 预算终止器
prompt = f"目标: {goal}\n历史:\n" + "\n".join(history[-5:])
action = call_llm(prompt)
observation = run_shell(action)
history.append(f"> {action}\n{observation}")
if verify(): # ② 成功终止器
return "success", step, history
if observation == prev_obs: # ③ stall 终止器
stall += 1
if stall >= STALL_LIMIT:
return "stalled", step, history
else:
stall = 0
prev_obs = observation
return "budget_exhausted", MAX_STEPS, history
你可能注意到 v3 里已经悄悄 history[-5:] 截断了。这是廉价做法。正确做法是每一步把新信息挤压成结构化 state:
@dataclass
class State:
goal: str
facts: list[str] # 我学到的确定的事实
last_observation: str # 最近一次的 raw output
open_hypotheses: list[str] # 我还不确定的猜测
def update_state(state, action, observation):
# 用一个便宜的小模型/规则做抽取(不是把 raw 塞回去)
new_facts = extract_facts(action, observation) # 例如 regex 抓错误名
state.facts.extend(new_facts)
state.last_observation = observation[:500] # 只留摘要
return state
def render_prompt(state):
# prompt 里给模型看的是 state,不是流水账
return f"""目标: {state.goal}
已确认的事实:
{chr(10).join(f'- {f}' for f in state.facts)}
最近一次执行返回:
{state.last_observation}
下一步动作是什么?"""
最后一件事,加起来只有 3 行,但决定你能不能优化这个 loop:
import json, time, pathlib
def agent_v5(goal, run_id):
trace_path = pathlib.Path(f"traces/{run_id}.jsonl")
trace_path.parent.mkdir(exist_ok=True)
state = State(goal=goal, facts=[], last_observation="", open_hypotheses=[])
for step in range(MAX_STEPS):
t0 = time.time()
action = call_llm(render_prompt(state))
observation = run_shell(action)
state = update_state(state, action, observation)
with trace_path.open("a") as f:
f.write(json.dumps({
"step": step,
"action": action,
"observation": observation[:2000],
"state_facts": state.facts,
"latency": time.time() - t0,
}) + "\n")
if verify(): return "success", state
# ... stall + budget 同 v3
trace.jsonl。你会立刻看到"哪一步蠢"——那就是下一轮要改的地方。这比在 prompt 里加"你要仔细思考"有用 100 倍。
找一个你现在正在用 harness 单次调用做的事(哪怕很小,比如"生成一段 SQL"、"重命名一批文件"),照下面这个顺序改造:
for step in range(10),里面调你原来的函数。goal / facts / last_obs 三个就够。trace.jsonl 落盘。