基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
技术实践2026-10-25 15:26

用 C# 实现 Agent 工具调用闭环

dev.to · AI#C##工具调用#Agent
Editor brief · 编辑速览

文章使用 Anthropic 官方 SDK,讲解工具定义、调用分发、结果回传及失败处理,串起完整的工具调用循环。作者称代码已在 Anthropic 13.2.0 和 PostgreSQL 18 环境编译运行。

文章思维导图
Knowledge map
拖拽缩放
AI deep read

AI 深度解读

模型能回答订单状态,不代表它能查询订单。Function Calling 补上的是这段执行链路:模型提出结构化调用,应用执行真实代码,再把结果交回模型。这篇文章用 C# 和 Anthropic SDK 拆解了完整循环,重点在消息如何衔接、失败如何恢复,以及写操作怎样避免重复执行。

对前端和全栈开发者,它的价值不只在 C# 示例。理解这套协议后,你更容易判断一个 Agent 框架替你处理了什么,又有哪些数据库权限、超时和审批问题,仍然得由业务代码负责。

模型负责提出调用,应用负责真正执行

在 Messages API 中,工具调用属于对话消息的一部分。模型返回 stop_reason: "tool_use",同时给出一个或多个 tool_use 内容块,每块包含调用 id、工具名和 JSON 参数。

应用收到后执行数据库查询或内部 API,再通过下一条 user 消息提交 tool_result。模型看到结果,可能继续调用工具,也可能用 stop_reason: "end_turn" 结束。

这里的 user 消息是协议中的消息角色,不代表用户又手动输入了一次。

相较于让模型生成一段文字,再用正则猜测意图,这套机制有三个明确优势:参数有 Schema 约束,错误可以作为结果返回,同一轮还能提出多个工具调用。但模型没有直接获得执行权限,真正能做什么,仍由应用暴露的工具决定。

作者使用官方 Anthropic SDK,称所有代码都在 Anthropic 13.2.0 和 PostgreSQL 18 上编译运行过。安装命令是 dotnet add package Anthropic。这里有个依赖陷阱:NuGet 上的 Anthropic 从版本 10 起才是官方 Claude SDK;3.x 及以下属于社区 tryAGI SDK,后者现在使用 tryAGI.Anthropic,两套 API 完全不同。

工具定义包含名称、描述和输入 Schema。示例工具 get_order_details 要求提供字符串 order_id,格式示例为 ORD-20261234;可选布尔参数 include_line_items 决定是否返回明细。描述还要明确告诉模型,查询订单状态、送达日期和商品内容时应调用它。

工具描述是模型选择工具的依据,不能只当成人类阅读的接口注释。

SDK 用 ToolUnion 容纳自定义工具和内置工具,Tool 可以隐式转换,因此 [getOrderTool] 能直接传给 MessageCreateParams.Tools。严格格式也可以通过 InputExamples 提供真实参数示例。

循环最容易错的地方,是调用与结果没有完整配对

原文循环通过 client.Messages.Create 请求模型,配置为 Model = "claude-sonnet-5-5"、MaxTokens = 4096,最多运行 10 轮。这里的模型名是原文配置;文章没有给出它的可用范围说明。

模型响应后,应用首先要把完整 assistant 消息写回历史,包括其中的 tool_use。随后执行所有调用,并把全部结果放进同一条 user 消息,每个结果都用 ToolUseID 对应此前的调用。

下面这段代码解决并行执行、单工具超时和结果集中回传的问题:

            var resultTasks = toolUseBlocks.Select(async block =>
            {
                try
                {
                    using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
                    cts.CancelAfter(TimeSpan.FromSeconds(30)); // 每个工具的超时限制

                    var result = await dispatcher(block.Name, block.Input, cts.Token);
                    return new ToolResultBlockParam
                    {
                        ToolUseID = block.ID,
                        Content = JsonSerializer.Serialize(result)
                    };
                }
                catch (Exception ex)
                {
                    // 返回结构化错误,让模型有机会恢复
                    return new ToolResultBlockParam
                    {
                        ToolUseID = block.ID,
                        Content = JsonSerializer.Serialize(new { error = ex.Message }),
                        IsError = true
                    };
                }
            });

            var toolResults = await Task.WhenAll(resultTasks);

            // 所有结果放入同一条 user 消息,每个调用对应一个结果块
            messages.Add(new UserMessageParam
            {
                Content = toolResults.Select(r => (ContentBlockParam)r).ToList()
            });

重点看 ToolUseID = block.ID 和最后的 messages.Add:只处理第一个调用,或者把结果拆成多条消息,都会破坏原文要求的配对关系。

响应中的 ContentBlock 不能直接作为请求参数使用。作者通过 ToParam 把 .Value 中的 TextBlock、ToolUseBlock 转成对应参数类型。开启 extended thinking 后,还必须保留 thinking blocks,示例转换函数需要补充分支。

作者对这部分做了一个明确测试:本地服务按 Messages API 格式先返回两个调用,一个查询订单,一个调用不存在的工具,然后返回 end_turn。他们检查了第二次请求,确认其中保留了两个调用,并用一条 user 消息回传两个结果;未知工具标记为 "is_error": true,没有让循环崩溃。

这验证了消息拼接和错误回传路径,但没有证明真实模型会稳定选择正确工具。

Schema 管参数形状,业务代码仍要守住边界

工具输入仍是不可信数据。原文 dispatcher 从 IReadOnlyDictionary<string, JsonElement> 读取参数,用 TryGetValue 处理缺失字段,再检查 ValueKind。如果直接调用 GetBoolean(),模型一旦传来字符串,就可能抛异常。

订单号通过 ^ORD-\d{8}$ 校验后,才进入 EF Core 查询。查询使用 AsNoTracking(),只有需要明细时才增加 Include。不能写成 Include(o => includeLineItems ? o.LineItems : null):它虽然能编译,但 EF Core 要求明确的导航属性路径,运行时会抛出 InvalidOperationException。

返回值也应保持克制。示例只返回订单号、状态、按 yyyy-MM-dd 格式输出的预计送达日期,以及按需提供的明细,避免把实体关系图整包送回模型。

错误同样需要设计。订单不存在是模型可以解释的业务结果,堆栈信息通常没有帮助。原文用 JsonSerializer.Serialize 序列化异常,避免引号破坏 JSON;但直接返回 ex.Message 是否合适,还要看其中有没有内部信息。

写工具则多了一层要求:超时不等于操作没发生。cancel_order 第一次可能已经成功,只是结果没送回来。模型重试时,处理器必须通过幂等键或检查后执行的逻辑,保证重复调用不会产生重复影响。

生产环境还需要只读凭证、通过 DI 注入服务令牌、记录工具名和耗时等结构化日志。令牌不能进入 Schema 或对话。工具返回的 50 KB JSON 会占用后续轮次的上下文,因此可以拆成摘要工具和详情工具,让模型按需获取。

涉及发邮件、扣款、取消订单或修改生产数据,作者建议先返回 {"status": "pending_approval", "approval_id": "..."},确认后再执行。

我的判断

我认同先看懂消息循环,再引入 Semantic Kernel 或 Microsoft.Extensions.AI。它能帮助开发者定位问题:到底是模型没选工具、参数不对,还是结果回传出了错。不过,作者没有给出这些方案的对照数据,不能据此认定直接使用 SDK 更可靠或更省成本。

示例也还有几处工程前提。CancelAfter 发出的是取消信号,下游代码必须响应 CancellationToken,才可能及时退出;catch (Exception) 会把取消也当成普通工具错误,真实项目要区分用户取消、工具超时和业务失败。

并行执行也不宜直接照搬到全部工具。独立查询适合并发,存在先后依赖或修改同一份数据的写操作,需要另外安排执行顺序。原文没有展开这部分。

另一个缺口是用户级授权。只读数据库账号限制了写入能力,却不能保证某位用户有权查询某张订单。示例查询条件只有订单号,实际服务还需要结合登录身份或租户范围过滤。

因此,这篇更适合作为协议与处理器设计的起点。作者提供了编译运行声明和一次本地协议测试,尚不足以支撑整套方案已经完成生产验证。

能拿来做什么

  • 给现有 AI 助手增加工具时,可以先选一个只读查询,写清工具描述、必填参数和业务错误,再检查调用历史与结果是否完整配对。
  • 在 Node 或其他技术栈实现循环时,可以保留原文的轮数上限、单工具超时和集中回传规则,并补测一次返回多个调用、包含未知工具的情况。
  • 给订单等业务工具接入数据库前,应把身份校验、参数校验和字段裁剪放进处理器,让模型只能获取当前用户有权查看的必要数据。
  • 增加写操作前,应先确定幂等策略和审批后的执行入口,明确超时重试会发生什么,再让模型调用它。
Original source

本文由 AI 通读原文后重新编写,观点部分为站内判断,细节以原文为准,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
把多个编程智能体的审批集中到网页
下一篇
Agent 更难识别格式正确的错误结果

资讯速览

dev.to · AI · 2026-10-25 15:26

AI 评分 8.0 / 10

相关标签

#C##工具调用#Agent

继续阅读

  • 用业务提示词并排评测多家大模型
  • 把多个编程智能体的审批集中到网页
  • Agent 更难识别格式正确的错误结果
  • 用精简交接记录延续 AI 编程上下文
  • Agent 审批必须绑定实际执行参数