分享|当你按下enter之后,agent背后发生了什么?
132
发布于 北京

1784647318-yZUUHH-1784647317104361.jpeg

平时使用 Coding Agent 时,我们只看到两件事:输入一句话,然后屏幕上慢慢出现回复。

但真正让它区别于普通聊天机器人的,并不是“调用了一次大模型”,而是中间那套不断循环的 Agent Loop:整理上下文、请求模型、执行工具、回填结果,再请求模型,直到得到最终答案。

最近我在写一个本地 Python Coding Agent——FirstCoder。下面以“读取 README 并总结”为例,拆开一条用户消息从进入 TUI 到完整回复的全过程。

一、用户按下 Enter 后,消息先去哪里?

Textual TUI 会先读取输入框内容,显示用户消息,然后启动一个异步任务。UI 不直接调用模型,而是把消息交给 Runtime,再由 Runtime 为这一轮创建 AgentLoop。

第一件发生的事也不是网络请求,而是把用户消息写入 append-only 的 Session Log。

也就是说,“读取 README 并总结”会先成为一条 user_message 事件。后续即使程序退出,FirstCoder 也可以根据日志重建这轮会话,而不是只依赖内存里的一份 messages 数组。

二、真正发给模型的,不只有这一句话

AgentLoop 会从 Session Log 重建当前会话,再由 ContextBuilder 投影成模型能理解的 messages。

一次请求大致包含:

{

"model": "当前选择的模型",

"messages": [

{"role": "system", "content": "项目规则、AGENTS.md、权限策略、Skills……"},

{"role": "user", "content": "历史消息或压缩后的摘要"},

{"role": "assistant", "content": "历史回复"},

{"role": "user", "content": "读取 README 并总结"}

],

"tools": [

{"name": "view", "description": "读取文件", "parameters": "JSON Schema"},

{"name": "grep", "description": "搜索代码", "parameters": "JSON Schema"}

],

"tool_choice": "auto",

"stream": true

}

所以模型拿到的不是一个孤零零的问题,而是“系统规则 + 会话历史 + 最新消息 + 可用工具说明”。工具只以 Schema 的形式发给模型,模型知道自己可以调用什么,但真正的工具仍在本地执行。

三、模型第一次返回的,可能不是答案

如果任务只需要语言知识,模型可以直接返回文本,Agent Loop 写入 assistant_message 后结束。

但“读取 README”需要真实文件内容。此时模型第一次返回的通常是一个 tool_call:

{

"id": "call_view",

"name": "view",

"arguments": {

"path": "README.md",

"limit": 200

}

}

在流式模式下,工具参数可能分散在多个 chunk 中到达。FirstCoder 会先把这些片段按 index 累积,只有收到完整 tool_call 并确认参数是合法 JSON 后,才允许进入执行阶段。半截参数不会被猜测或执行。

四、工具不是模型执行的

模型只负责提出“我想调用 view”。AgentLoop 会先把这条 assistant tool_call 写入日志,然后把它交给本地 ToolExecutor。

ToolExecutor 会检查:

1. 工具是否存在;

2. 参数是否合法;

3. 当前权限策略是允许、拒绝,还是需要询问用户;

4. 如果是写文件,是否需要先生成 Diff 供用户审查;

5. 只读工具能否安全地并行执行。

通过检查后,真正读取 README.md 的是本地 Python 工具,而不是大模型。执行结果随后被保存为一条 role=tool 的 tool_result,并且带回原始 tool_call_id。

会话顺序会变成:

user:读取 README 并总结

assistant:调用 view(path="README.md")

tool:返回 README 的真实内容

这个顺序不能乱。模型下一次请求必须看到合法的“assistant tool_call → tool result”配对,否则不少 Provider 会直接拒绝请求。

五、工具结果还要再发给模型一次

拿到文件内容并不代表任务结束。工具只返回事实,不负责组织最终答案。

AgentLoop 会重新构造上下文,再次请求模型。这次 messages 里多了两项:模型刚才提出的 tool_call,以及本地工具返回的 tool_result。

模型现在终于看到了 README 的真实内容,于是可以生成最终总结。如果它发现还需要搜索其他文件,就会再次返回 tool_call,Agent Loop 继续下一轮:

模型 → 工具调用 → 本地执行 → 结果回填 → 再问模型

直到某次模型响应不再包含 tool_calls,才被认为是这一轮的最终回复。

六、为什么回复能一个字一个字显示?

Provider Adapter 会把不同模型厂商的流式协议统一成内部事件:

message_started

text_delta

tool_call_delta

tool_call_completed

message_completed

TUI 收到 text_delta 后立即更新界面,所以用户不用等整段答案生成完。流结束后,完整 ChatResponse 仍会被保存到 Session Log,避免界面显示了一份、会话记录里却是另一份。

七、整个 Agent Loop 可以压缩成这条链路

用户输入

→ 写入 Session Log

→ 重建上下文

→ 拼接 System Prompt、历史消息和 Tools Schema

→ 请求模型

→ 模型直接回答?写入最终回复并结束

→ 模型请求工具?本地检查权限并执行

→ Tool Result 写回上下文

→ 再次请求模型

→ 直到没有新的 Tool Call

→ 流式显示并持久化最终回复

这也是我写 FirstCoder 时最想弄明白的东西:Coding Agent 并不是“LLM 加几个函数”这么简单。真正决定它是否可靠的,是工具调用顺序、权限边界、上下文重建、流式事件和异常恢复能不能组成一个闭环。

FirstCoder 是一个本地 Python Coding Agent。我把 AgentLoop、Providers、Tools、Permissions、Session 和 Context 分成了独立模块,希望它既能运行,也能让想学习 Agent 工程的人顺着源码读明白。

项目地址:https://github.com/KomorGiaoGiao/FirstCoder

欢迎提issue和pr,更欢迎⭐(厚颜无耻)

评论 (1)