第 05 篇 · 事件接口:让 Agent 的执行过程可以被外部接管
用户给它一个任务,它会先问模型下一步怎么做。模型要是决定用工具,程序就执行工具,工具结果回来以后,再交给模型继续判断。这样一轮一轮的循环跑下去,直到模型不再调工具,直接给出最终回答。
我们这套最小Agent已经可以工作了,给它配置对应的工具,它就可以开始干活了。
但是如果我们要继续开发完善这个Agent,我们很快就会遇到一个新问题,Agent不止要看最终的结果,模型的思考,中间的执行过程都要展示出来。
比如模型正在输出什么文字,准备调用哪个工具,参数长什么样,工具执行完返回了什么。
这些内容,我们现在都是通过 print 打印 直接在终端里面输出。终端输出是我们开发调试阶段最好的观察工具,但它他没有办法支持后续的工程演进。
所以这一篇我们做一个很小改动,把print 改成 事件产出:
让 Agent loop 不再直接打印过程,而是一边执行,一边产出事件。
代码上大概就是这样:
yield {"type": "token", "content": "..."}
yield {"type": "tool_start", "name": "read_file", "args": {...}}
yield {"type": "tool_result", "name": "read_file", "output": "..."}
看起来只是把 print 换成了 yield,结果却完全不同。
print 是写死在终端里的展示方式,调用方只能看到打印出来的文字,没法自己处理。yield 把函数变成了生成器,每跑一步就产出一个事件,形成可以被逐步消费的事件流。这个事件流就是一个灵活的输出接口:
CLI 可以拿它来逐行打印;
Web 服务可以把它转成 SSE 推给前端;
测试可以逐事件断言执行过程;
以后想做流式中断、续跑、更复杂的事件驱动逻辑,也可以沿着这个方向继续改造。
这一篇结束后,我们会得到一个新的入口:
run_agent_events(user_message)
它不再是等任务跑完再返回一个最终答案,而是边执行边把事件吐出来。
yield 实现原理
这里我们先来看一下yield的实现原理。
普通函数是一次性返回结果:
defrun_agent(user_message: str) -> str:
...
return final_answer
调用这个函数时,调用方只能等程序跑完,才能拿到一个最终的结果,我们刚刚也讨论了,Agent 的执行过程不是一下子完成的。模型会先输出一点文本,可能再调用工具,工具跑完以后又进入下一轮。中间每一步都需要被记录,被展示出来
这时我们就可以用 Python 的 generator。
只要一个函数里出现了 yield,它就不再是普通函数,而是一个生成器函数:
defrun_agent_events(user_message: str):
yield {"type": "token", "content": "我先看一下文件。"}
yield {"type": "tool_start", "name": "read_file", "args": {"path": "hello.txt"}}
yield {"type": "tool_result", "name": "read_file", "output": "hello agent"}
yield {"type": "done"}
在外面通过for循环来打印 yeild 的内容
for event in run_agent_events("读取 hello.txt"):
print(event)
这段代码不是等 run_agent_events 全部执行完才开始打印。
它的运行方式是一来一回的:
run_agent_events 跑到第一个 yield
把 token 事件交给外面的 for 循环
外面的 for 循环打印这个事件
再回到 run_agent_events 继续往下跑
跑到下一个 yield
再把 tool_start 事件交出去
...
所以我们可以把 yield 理解成:函数先暂停一下,把当前事件交给外面;外面处理完以后,函数再从暂停的地方继续跑。
run_agent_events 是事件生产者。它不关心事件最后怎么用,只负责在合适的时机 yield 内容出来。
CLI 里的这段代码是事件消费者:
for event in run_agent_events(user_input):
print_event(event)
每次 Agent loop 里 yield 一个事件,外面的 for event 就收到一次,然后交给 print_event。如果以后换成 Web,也只是把 print_event(event) 换成 to_sse(event)。Agent 主循环不用改。