猪头少年 - 云南AI专家 - 云南独立开发者

Engineering note

Agent 到底是什么:它不是一次模型调用

这是「Agent 工程实战」的第 2 篇。专题从一个能聊天、能调工具的 .NET Agent 出发,逐步补齐可靠性、安全、测试与发布能力。

本篇要解决的问题: 把消息、模型决策、工具执行和状态管理拆开,理解 Agent 的最小闭环。

返回专题路线


本章定位:本章回答 Agent 与普通聊天程序的根本区别。重点不是给 Agent 贴一个更酷的名字,而是理解“决策—执行—观察—再决策”的闭环,以及模型和程序各自应该承担什么责任。

建议阅读方式:先通读原理,再对照当前项目源码,最后完成本章实践。读完后,你应该能用一个具体任务解释模型为什么需要工具、程序为什么不能盲信模型。

本章导读

本章回答 Agent 与普通聊天程序的根本区别。重点不是给 Agent 贴一个更酷的名字,而是理解“决策—执行—观察—再决策”的闭环,以及模型和程序各自应该承担什么责任。

本章采用“源码观察 → 概念拆解 → 工程改造 → 实践验证”的顺序。示例中的接口和代码骨架用于说明设计方向,真正提交代码时应结合项目当前状态逐步落地。

2.1 Chatbot 与 Agent 的区别

普通聊天程序通常是:

用户问题 -> 模型回答 -> 结束

Agent 则是:

用户目标 -> 模型决定下一步 -> 执行动作 -> 观察结果 -> 再决定下一步

因此,Agent 的核心不是某一段提示词,而是一个循环:

flowchart TD
    S[接收用户目标] --> C[构造上下文]
    C --> D[请求模型决策]
    D --> Q{模型要调用工具吗?}
    Q -->|否| F[输出最终回答]
    Q -->|是| V[校验工具调用]
    V --> X[执行工具]
    X --> O[记录工具结果]
    O --> C

2.2 BooAgent 中的 Agent Loop

ChatStreamAsync 做了几件关键的事:

  1. 把用户消息加入 _messages
  2. 把消息和工具描述发给模型;
  3. 读取 SSE 中的 contenttool_calls
  4. 当模型要求工具时,拼接完整参数;
  5. 通过反射调用本地方法;
  6. 把工具结果以 role = "tool" 的消息加入历史;
  7. 再次请求模型,直到得到最终文本。

这就是最小 Agent。实际生产版本还需要明确循环最大步数、取消信号、超时、失败策略和终止条件。

2.3 三类消息

当前代码中的 Message 对应工具调用协议里的三类消息:

  • system:定义 Agent 的身份和行为规则;
  • user:用户的目标或问题;
  • assistant:模型输出,可能含文本或工具调用;
  • tool:本地工具执行结果,并通过 tool_call_id 对应模型发出的调用。

一个完整的工具调用消息序列是:

user: 请读取 notes/today.md
assistant: tool_calls = ReadFile({"filePath":"notes/today.md"})
tool: {"Message":"成功","Result":"..."}
assistant: 文件内容是……

2.4 Agent 的三个状态

工程实现时可以把 Agent 状态简化为:

public enum AgentTurnState
{
    WaitingForUser,
    AskingModel,
    ExecutingTools,
    Completed,
    Failed,
    Cancelled
}

状态不是为了增加复杂度,而是为了让日志、UI 和错误恢复知道当前发生了什么。

2.5 本章小结

把 Agent 看成一个“受约束的决策循环”,后面的配置、工具、记忆、权限和评测都会自然地找到位置。

2.6 Agent 的决策与程序的执行

初学者常见的误解是:模型“调用了”某个工具。更准确的说法是:模型提出了一份结构化的调用建议,程序决定是否接受并执行。

这一区别非常重要。模型可能:

  • 选择一个不存在的工具;
  • 生成无法解析的 JSON;
  • 传入超出权限范围的路径;
  • 误判一个有副作用的操作是安全的;
  • 在工具失败后重复同一个调用。

因此,Agent 的控制权应该分成两部分:

模型:决定“下一步可能要做什么”
程序:验证“这一步是否允许、参数是否有效、如何执行”

把程序当作策略执行层,后面的安全设计就不会变成补丁。

2.7 用一个具体任务理解循环

任务:

找出项目中所有 C# 文件,并统计包含 ToolAttribute 的文件。

一个可能的执行过程是:

  1. 模型选择 GetFiles,参数是项目目录;
  2. 程序检查目录是否在 workspace 内;
  3. 工具返回文件列表;
  4. 模型发现还需要读取若干文件;
  5. 程序逐个执行读取,或拒绝超出数量的批量请求;
  6. 模型汇总结果并输出结论。

这个任务说明 Agent 的能力来自“工具组合”,而不是来自某一个特别聪明的工具。工具越多,组合空间越大,循环控制和权限约束就越重要。

2.8 练习:区分三种输出

分别写出以下三种结果应当如何处理:

  • 模型正在输出一句话的中间字符;
  • 模型正在输出工具参数的一部分;
  • 工具已经执行完成,但模型还没有给最终回答。

推荐答案是:第一种可以展示,第二种只累积不执行,第三种继续进入下一轮模型请求。把这三种状态混成一个字符串,是后续难以测试的主要原因之一。

2.9 本章产出

完成本章后,应能画出 Agent Loop,并能解释“模型决策”和“本地执行”的边界。建议把这张图作为项目 README 的设计说明,未来每次重构都用它检查是否破坏了核心循环。

单篇实战作业

实践:写一个最小任务说明,包含用户输入、允许工具、禁止工具和完成条件,再用它检查本章概念。

建议把作业拆成一个独立提交,并在提交说明中写清楚:改动前的行为、改动后的行为、验证命令、尚未解决的风险。读完后,你应该能用一个具体任务解释模型为什么需要工具、程序为什么不能盲信模型。

章节复盘

复盘问题:当模型提出危险动作时,究竟是哪一层拥有最终否决权?把答案写成接口,而不是写成口号。

本章的完成标准不是把所有设计一次性做完,而是能把它变成项目中的一个明确边界,并为下一章留下可验证的接口。


下一篇:第 3 篇《项目启动与第一条消息:先把最小闭环跑起来》

如果你正在把 Agent 放进真实工作流,建议完成本篇的实战作业后再继续:每一步都应留下可验证的代码、测试或运行记录。

Next step

不要停在单篇文章。

沿主专题继续阅读,查看同一问题从概念到实战的完整路径。

返回「Agent 工程实战」路线