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

Engineering note

先看懂项目,再开始扩展:给 Agent 画一张代码地图

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

本篇要解决的问题: 从一次真实请求出发,建立 Agent 的代码地图,并区分“能运行”和“工程可用”。

返回专题路线


本章定位:本章是一篇项目考古文章。你会从目录、入口和一次真实请求出发,建立 BooAgent 的代码地图,然后把“当前能运行”和“距离工程可用还差什么”区分开。

建议阅读方式:先通读原理,再对照当前项目源码,最后完成本章实践。阅读时不要急着重构。先画出执行路径,再对照源码验证每个节点。

本章导读

本章是一篇项目考古文章。你会从目录、入口和一次真实请求出发,建立 BooAgent 的代码地图,然后把“当前能运行”和“距离工程可用还差什么”区分开。

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

1.1 当前项目做了什么

项目是一个没有第三方 NuGet 依赖的 .NET 8 控制台程序。入口在 Program.cs:创建 BooAgent,然后调用 Start()

核心代码可以分成四层:

层次 当前文件 职责
应用入口 Program.cs 启动 Agent
Agent 编排 Agent/BooAgent.cs 收集工具、读取用户输入、把请求交给模型处理器
模型适配 Agent/ModelHandle.cs HTTP 请求、SSE 流解析、消息历史、工具调用循环
工具实现 Agent/Tools/*.cs 文件读写、命令执行,以及工具元数据特性

当前架构可以抽象成下面的流程:

flowchart LR
    U[用户输入] --> A[BooAgent]
    A --> R[反射扫描工具]
    A --> M[DeepSeekStreamHandler]
    M --> P[模型 HTTP API]
    P -->|文本增量| M
    P -->|tool_calls| M
    M --> T[本地工具方法]
    T --> M
    M --> P
    M --> A
    A --> U

1.2 当前版本的能力边界

已具备:

  • 控制台交互;
  • 多轮消息列表;
  • 流式输出文本;
  • 从模型增量拼接工具调用参数;
  • 通过特性和反射自动发现工具;
  • 文件读、文件写、目录扫描;
  • 执行外部命令并返回标准输出。

尚未具备:

  • 安全的密钥和模型配置;
  • 工具参数的严格类型转换和校验;
  • 工具权限、审批和沙箱;
  • 可靠的异常恢复、超时和取消;
  • 会话持久化和上下文压缩;
  • 测试、日志、指标和评测;
  • Web 接口、并发会话和发布方式。

这正好构成了本书的主线:先保留最小闭环,再逐项替换脆弱部分。

1.3 第一次运行前需要知道的事

当前 DeepSeekStreamHandler 的默认 API Key 是空字符串,接口地址和模型名称直接写在 ModelHandle.cs 中。也就是说,项目虽然能编译,但不能在没有配置服务地址、密钥和模型的情况下正常完成线上对话。

建议后续首先把这三项移出代码。不要把真实密钥提交到仓库,也不要把密钥写入教程中的示例文件。

1.4 本章小结

一个 Agent 不等于“调用一次模型”。它至少包含:消息状态、模型决策、工具执行和结果回传。当前 BooAgent 已经有这个骨架,后续工作是把每个隐含假设变成明确的接口和规则。

1.5 从代码地图走一遍真实执行路径

理解项目最有效的方式不是从类名开始背,而是跟踪一次请求。

假设用户输入:

请读取 Agent/ModelHandle.cs,并告诉我它如何处理工具调用。

程序首先在 Start() 中读到这行文本,然后进入 HandleMessage()HandleMessage() 没有自己处理网络细节,而是把文本、工具定义和工具实例表交给 _handler.ChatStreamAsync()

进入 ChatStreamAsync() 后,用户消息会追加到 _messages。这里的 _messages 是会话的短期记忆,也是下一次请求的上下文。之后程序构造匿名对象并序列化为 JSON,发送到模型服务。

模型如果判断需要读取文件,不会返回文件内容,而会返回一个或多个工具调用片段。程序在 toolCallAccumulator 中按调用索引累积名称和参数。等流结束并确认 finish_reason 是工具调用后,程序才真正执行 ReadFile

执行完成后,工具结果被包装为 role = "tool" 的消息,再次加入 _messages。下一次模型请求同时看到了用户问题、自己的工具调用和工具结果,于是可以生成最终解释。

这条路径中的每一个对象都有自己的职责:

对象 应该回答的问题
Message 对话上下文中发生了什么?
Tool 模型可以请求什么能力?
_toolInstances 程序如何找到实际实现?
DeepSeekStreamHandler 如何与模型协议通信?
BooAgent 如何把用户交互串起来?

后续重构时,优先保持这些问题的边界,而不是机械地按文件拆类。

1.6 一次代码阅读练习

打开 Agent/ModelHandle.cs,尝试不运行程序,只回答下面五个问题:

  1. 用户消息在哪一行进入历史?
  2. 文本增量在哪一行返回给控制台?
  3. 工具调用参数为什么不能在收到第一段时立即执行?
  4. 工具结果如何和原始调用对应起来?
  5. 如果工具执行抛出异常,异常会在哪一层被捕获?

如果第 5 个问题没有明确答案,就说明当前系统还缺少统一的错误边界。这种阅读题会贯穿后面的章节。

1.7 本章练习:绘制自己的架构图

在项目根目录新建一个个人笔记,画出“用户输入、模型请求、工具调用、工具结果、最终回答”五个节点,并在每条箭头旁写上实际的 C# 方法名。下一次新增功能时,先把它放到图上,再决定应该修改哪个类。

本章的完成标准不是记住目录,而是能够向别人解释:一次工具调用从哪里开始,经过哪些对象,在哪里结束。

单篇实战作业

实践:为本章画一张“当前代码—目标架构”对照图,标出至少三个暂时不改但必须记住的风险。

建议把作业拆成一个独立提交,并在提交说明中写清楚:改动前的行为、改动后的行为、验证命令、尚未解决的风险。阅读时不要急着重构。先画出执行路径,再对照源码验证每个节点。

章节复盘

复盘问题:如果新同事只能读这一章,他能否解释当前项目的边界、下一步改造顺序和一个真实故障的定位路径?

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


下一篇:第 2 篇《Agent 到底是什么:它不是一次模型调用》

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

Next step

不要停在单篇文章。

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

返回「Agent 工程实战」路线