前两天有个做上位机开发的朋友问我:C#项目里想接AI对话能力,是不是非得自己封装HTTP,或者绕道起一个Python服务中转?这个问题我太熟悉了,做.NET这块的开发者应该都有同感:Python生态里这类封装随手就是一大把,到了C#这边,要么绑进某个全家桶式框架里,要么只是把请求简单包装一下就扔给你,重试、流式解析、工具调用这些工程问题全得自己补。DSH-Sharp(DeepSeek-Harness-Sharp 的缩写)就是为解决这件事做的——一个面向 C#/.NET 的轻量级 LLM 工具集,把模型接入、流式输出、工具调用、故障容错这些事用纯 C# 的方式做干净。这篇是它的发布说明,我会把设计思路、核心功能、接入方式,以及我实测过程中踩过的坑全部写出来。它适合想在 C# 项目里直接集成 AI 能力的开发者,尤其是桌面端、上位机、企业内部服务这类存量系统的维护者。
1. 为什么会有 DSH-Sharp:C# 生态里的一个真实痛点
1.1 从一次"临时接一个 AI 服务"的经历说起
之前有一个带触摸屏的工控软件项目,需要在现场设备边上提供一段语义问答能力,用户通过本地界面问设备状态,程序返回结果。最初的方案很直白:拿 HttpClient 直接调模型接口,POST 一段 JSON,拿回另一段 JSON,解析出来显示到界面上。
听起来很简单,真正写起来就不对劲了。超时了要不要重试?模型回答是一点一点吐出来的,流式响应怎么解析?如果模型想反过来调本地方法查设备电压,这个链路怎么闭环?日志里到底该记什么、又不该记什么?换一个模型服务商,是改几十处调用点还是一次配置切换?那几天我基本就是在挨个补这些问题,越补越觉得,缺的不是"一个能调接口的类",而是一套把工程问题考虑周全的基础工具。
市面上的方案我也看过一轮。有些框架确实功能多,但依赖重、概念多,为了接一个对话能力要把一整套抽象都引进来,跟存量 C# 系统整合的成本不低。有些轻量封装又实在太"裸",本质上只是帮你拼了个请求,后面全得自己来。DSH-Sharp 的定位就卡在中间:不搞全家桶,但把模型接入这件事从"能用"做到"好用"。
1.2 这不是又一层 SDK 封装,而是想解决四个问题
DSH-Sharp 的设计不是围绕"多包一层"展开的,而是围绕下面四个问题展开的,这也是我在实际项目里被反复折磨过的四个点:
- 接入成本:模型接口的通信协议对 C# 开发者来说其实不复杂,但每个项目都自己写一遍解析代码,后期维护就是灾难。我希望能有一个"配置一下就能用"的入口,最好十行代码内完成第一次对话。
- 工程化能力:调模型服务不是纯粹的本地函数调用,中间隔着网络、隔着重试、限流、超时、日志。这些在 Python 侧常见的容错能力,在 C# 封装里反而稀缺。
- 工具调用:这是最核心的诉求。模型本身不读数据库、不碰设备,只有让它能触发 C# 侧的真实方法,AI 才真正嵌进了业务系统。但这个调用-执行-回传的循环,如果每次都手写,太容易出错了。
- 可测试性:真实模型服务不是随时可用的,开发阶段频繁调用既不快也费钱。我希望本地有一种模拟运行的方式,在开发环境可以先跑通逻辑,再切到真实服务。
这四个问题决定了项目的基本面貌,也决定了它和简单 HTTP 封装之间的本质差别。
1.3 面向谁、不面向谁
先说面向谁。如果你正在维护一套存量 .NET 系统,比如 WinForms 上位机、WPF 桌面工具、企业内部服务,需要在不推翻现有架构的前提下加入 AI 能力,DSH-Sharp 就是合适的。如果你做的是工控集成、设备数据采集、本地文档问答这类偏"业务落地"的场景,它能帮你把模型接入这件事收敛成一个模块,而不是散落在业务代码里的几十个请求调用点。
再说说它不打算服务的人群。如果你的核心诉求是高并发 Web API,对吞吐和延迟有极致要求,建议还是用更成熟的网关方案,DSH-Sharp 的目标不是抢那个生态位。如果只是一个玩票性质的 Demo,只想贴一段代码看效果,那确实不需要引入一个库,直接写请求反而更直观。
2. DSH-Sharp 的整体架构与设计取舍
2.1 模块划分与命名空间
DSH-Sharp 不是一个大而全的单体类库,从第一天起就按职责拆成了几个模块。模块之间依赖关系是单向的:客户端在最上层,工具注册在中间,配置和诊断是最底层的基础设施。这样的好处是,你完全可以只引入客户端和配置模块,而不启用工具调用;也可以单独把工具模块接到自己的请求链路上,不强制使用整套方案。
各模块的命名空间和职责如下:
| 模块 | 命名空间 | 主要职责 |
|---|---|---|
| 客户端 | DshSharp.Client | 请求组装、响应解析、流式读取、重试与限流 |
| 工具注册 | DshSharp.Tools | 特性标注、反射扫描、JSON Schema 生成、执行调度 |
| 配置管理 | DshSharp.Config | 多数据源合并、热更新、密钥读取 |
| 诊断扩展 | DshSharp.Diagnostics | 结构化日志、追踪钩子、敏感信息过滤 |
| 本地模拟运行时 | DshSharp.LocalRuntime | 不联真实服务时的模拟对话、模拟工具返回 |
实际使用的时候,你会发现这个拆分直接影响体验。举个例子:早期我在测试工具调用时,为了调试 JSON Schema 的生成结果,需要反复改方法签名。如果把工具注册和客户端绑死,那每次改动都要跑一遍完整对话链路;拆开之后,可以直接在单元测试里调用 Schema 生成器,问题分钟级定位。
2.2 三条设计红线
架构设计最怕的就是"什么都能做,最后什么都做不深"。DSH-Sharp 在开发过程中立了三条红线,所有功能设计都先过一遍这三条:
- 不引入重量级第三方依赖。核心模块只依赖 System.Net.Http 和 System.Text.Json。这两样是 .NET 自带的,不存在版本地狱问题。宁可自己实现一些基础逻辑,也不为了省事拖进一堆包。实际开发体验是:在离线内网环境部署时,少一个依赖就少一个麻烦。
- 所有 IO 路径全部异步。DSH-Sharp 不提供任何阻塞式调用接口。C# 的 async/await 已经足够成熟,阻塞线程在 GUI 程序里会导致界面卡死,在服务端会浪费线程池资源,没有任何值得保留的理由。
- 默认安全,敏感信息不出边界。请求正文默认不进入日志,API 密钥只允许从环境变量或自定义配置器读取,不允许硬编码在配置文件中。这些不是可选项,而是默认行为。
这三条红线让我在后续迭代中省了很多纠结。每当有人提议"加一个同步方法方便调用吧"或者"把密钥直接写进本地配置吧",我都能用红线直接拒绝,而不是陷入无休止的讨论。
2.3 配置层的细节
配置层是整个库里看起来不起眼、但实际使用频率最高的部分。它支持四级配置源,按优先级从低到高分别是:内置默认值、配置文件、环境变量、代码级配置。合并规则很简单:后面的覆盖前面的。
举个例子,一份典型的配置文件长这样:
{ "model": "deepseek-chat", "endpoint": "https://api.example.com/v1", "timeoutSeconds": 30, "retry": { "maxAttempts": 3, "baseDelayMs": 500, "multiplier": 2, "statusCodes": [429, 502, 503] } }开发环境可以用DEV_MODEL环境变量覆盖模型名,测试环境可以用DSH_ENDPOINT覆盖接口地址,生产环境的密钥从环境变量里读。这样一种配置结构就能覆盖三种环境的切换需求,不需要为环境差异设计专门的配置中心。
配置热更新是后来补的功能。原来我以为"改配置就重启"没什么大不了,直到有一次现场调试,每改一次模型参数要重启整个上位机软件,来回折腾十几分钟,才意识到热更新的必要性。现在配置源实现了简单的文件监听,配置变更后通过事件通知客户端重建内部状态,全程不用重启进程。
3. 核心能力拆解:从请求封装到工具调用
3.1 类型安全的请求响应模型
我见过不少 C# 的模型服务封装,请求体拿 Dictionary 拼,响应体拿 JsonDocument 解析,键名拼错了或者字段类型变了,运行期才暴露。DSH-Sharp 选择用强类型模型来承载整个通信过程。请求对象、消息对象、响应对象、用量统计对象全部是公开的类,有完整的属性定义和 XML 注释。
这样做的好处是巨大的。IDE 智能提示能直接告诉你有哪些字段可用,编译期就能发现拼写错误,参数变化时编译器会强迫你去改调用点。举个最小例子,发起一次对话:
using DshSharp.Client; var client = DshClientBuilder.Create() .UseModel("deepseek-chat") .UseEndpoint("https://api.example.com/v1") .UseApiKey(Environment.GetEnvironmentVariable("DSH_API_KEY") ?? throw new InvalidOperationException("缺少DSH_API_KEY")) .Build(); var reply = await client.ChatAsync(new ChatRequest { Messages = { new ChatMessage("user", "用三句话介绍C#上位机开发") } }); Console.WriteLine(reply.Message.Content);这个例子覆盖了 DSH-Sharp 最常用的启动路径:构建器配置客户端,构造请求,异步调用,获取结果。整个过程中没有一处 JSON 手动拼接,也没有反射魔法,所有流转都发生在类型安全的边界内。
要说明一点,强类型并不意味着死板。遇到服务端返回额外字段的场景,响应对象里保留了Extensions字典,原始数据不会丢;遇到需要自定义请求头的场景,ChatRequest上也有开放入口。安全性靠类型保证,灵活性靠扩展点补齐,两者不冲突。
3.2 流式输出:SSE 解析和异步流
模型服务返回完整回答往往需要几秒甚至更长,如果界面上一直空白,用户体验很差。流式输出就是把大回答切成一小块一小块,边生成边推送,用户看到的是逐字输出的效果。服务端通常用 SSE(Server-Sent Events)格式传输,一段数据长这样:
data: {"id":"...","delta":"你好","finish_reason":null}两个换行符之间是一块数据。DSH-Sharp 内部实现了完整的 SSE 解析器,对外暴露的是IAsyncEnumerable,调用方用await foreach消费即可:
var request = new ChatRequest { Messages = { new ChatMessage("user", "写一段关于C#异步编程的说明") }, Stream = true }; await foreach (var chunk in client.StreamChatAsync(request)) { Console.Write(chunk.Delta); }await foreach是 C# 8 引入的异步流语法,配合取消令牌使用非常自然。GUI 程序里可以把CancellationTokenSource绑定到界面上的停止按钮,用户随时可以中断生成;服务端程序在做流式转发时,也能把这些数据包原样透传给前端。
流式解析的边界情况比预想多。最典型的就是半包和粘包:网络传输会拆包,一个data:块可能被分成两次到达,也可能两次响应合并成一次。解析器必须用缓冲积累数据,而不是简单按行切割。这块我写的时候吃了不少苦头,后面会专门讲。
3.3 工具调用:声明式注册与执行循环
工具调用是 DSH-Sharp 区别于普通 HTTP 封装的核心功能,英文通常叫 Function Calling。它的原理不复杂:模型在生成文本时,如果判断需要调用某个外部能力,不会直接写最终回答,而是返回一个"我要调用这个函数、参数是这些"的结构化指令。程序负责执行真实方法,把结果作为一条新消息回传给模型,模型基于结果继续生成回答。
DSH-Sharp 把这套流程做成了声明式风格。你只需要在方法上标注一个特性,剩下的 Schema 生成、参数序列化、循环控制全部由框架处理:
using DshSharp.Tools; [DshTool(Name = "GetPlcVoltage", Description = "查询指定PLC通道的当前电压值")] public static double GetPlcVoltage(string channelId) { // 这里替换成真实的PLC读取逻辑,比如Modbus请求 return 24.0; }注册时通过反射扫描程序集,把带特性的方法收集起来,自动生成模型需要的 JSON Schema。模型返回工具调用指令后,框架按名称找到对应方法,把参数从 JSON 反序列化成实际类型,调用执行,再把结果打包成回传消息。
执行循环不能无限跑。模型可能反复要求调用工具,形成死循环。DSH-Sharp 默认限制单轮对话最多执行 6 次工具调用,达到上限后返回错误并提示用户。这个限制在配置文件里可以调整,但我不建议调太高——多数业务场景 3 到 4 轮就能得到结果,6 次已经是安全边际了。
整个调用链路的代码逻辑大致如下:
var messages = new List<ChatMessage> { userMessage }; for (int i = 0; i < maxToolRounds; i++) { var response = await client.ChatAsync(new ChatRequest { Messages = messages }); if (!response.HasToolCalls) return response; messages.Add(response.Message); foreach (var call in response.ToolCalls) { var output = DshToolRegistry.Execute(call.Name, call.Arguments); messages.Add(new ChatMessage("tool", output, call.Id)); } }这段代码展示了工具调用的核心循环:对话-判断-执行-回传,直到模型不再请求工具为止。实际使用时,这些逻辑被封装在AutoRunAsync方法里,一行代码就能启动完整的"思考-行动-观察"链路。
3.4 故障转移、重试与限流
网络调用必须假设失败。DSH-Sharp 内置了基于指数退避的重试机制,默认行为如下:
| 场景 | 默认行为 | 说明 |
|---|---|---|
| HTTP 429(限流) | 重试,最多 3 次 | 退避递增:500ms、1s、2s |
| HTTP 502/503(服务不可用) | 重试,最多 3 次 | 与 429 共用退避策略 |
| HTTP 400/401/403 | 不重试 | 参数或鉴权问题,重试无意义 |
| 超时 | 不重试 | 避免重复提交造成的副作用 |
| 网络异常 | 重试,最多 2 次 | 连接层面的暂时性故障 |
为什么 400 和超时不重试,这个决策值得解释。400 类错误是请求本身的问题,重试一万次结果一样;超时则更微妙,因为请求可能已经在服务端开始执行了,客户端只是没等到响应,此时重试可能造成重复写入。按调用类型区分重试策略,比一刀切地"失败就重试"更稳妥。
限流不止针对服务端,也针对客户端自身。DSH-Sharp 允许配置并发上限,内部用SemaphoreSlim控制同时进行中的请求数。有一次我在测试时用 50 个并发任务打同一个接口,结果大量超时,服务端没垮,客户端自己先撑不住了。加上并发窗口后,超出部分排队等待,整体成功率反而上去了。
4. 十分钟把 DSH-Sharp 接进一个 C# 项目
4.1 包引入与最小启动代码
接入的第一步是引入包。DSH-Sharp 的客户端和工具注册分属两个不同的包,按需引入即可。在项目目录执行:
dotnet add package DshSharp.Client dotnet add package DshSharp.Tools如果只是做普通对话,引入第一个包就够了;需要工具调用再引入第二个。包本身的依赖面很小,不会拖一堆传递依赖进来,离线环境的部署包体积基本可控。
最小启动代码我已经在上面展示过了,核心就三步:构建客户端、构造请求、异步调用。这段代码放到控制台程序里就能跑通,不需要额外配置服务定位器,也不要求特定的宿主环境。对于把 AI 能力嵌进已有系统的场景,这种"即插即用"的方式成本最低。
有一个细节值得提一下:DshClientBuilder上的UseApiKey方法只从外部传入密钥,构造函数里不允许填写硬编码字符串。这是我在某个项目里被教训出来的——密钥写死在代码里,后来仓库权限泄漏,全组都得改密钥。现在 DSH-Sharp 直接把这个口子堵死。
4.2 一个带工具调用的实际例子:上位机查询场景
用一个贴近真实业务的例子展示完整接入过程。假设有一个上位机监控软件,需要支持用户通过自然语言查询设备电压。我们可以注册一个查询电压的工具方法,让模型根据用户的自然语言判断应该调用它:
using DshSharp.Client; using DshSharp.Tools; [ToolContainer] public static class DeviceTools { [DshTool(Name = "GetPlcVoltage", Description = "查询指定PLC通道的当前电压值,参数channelId为通道编号,如CH-01")] public static string GetPlcVoltage(string channelId) { // 换成实际的MODBUS/OPC UA读取逻辑 double voltage = ReadVoltageFromDevice(channelId); return $"通道{channelId}当前电压为{voltage:F2}V"; } } var client = DshClientBuilder.Create() .UseModel("deepseek-chat") .UseEndpoint("https://api.example.com/v1") .UseApiKey(Environment.GetEnvironmentVariable("DSH_API_KEY")!) .WithToolsFromAssembly(typeof(DeviceTools).Assembly) .Build(); var answer = await client.AutoRunAsync("帮我查一下CH-01通道的电压是多少"); Console.WriteLine(answer.Message.Content);模型收到问题后,会先返回一个工具调用指令:调用GetPlcVoltage,参数channelId为CH-01。框架执行完方法,把电压结果回传给模型,模型再组织成自然语言回答。整个过程一次AutoRunAsync调用就完成了。
这个例子能看出 DSH-Sharp 工具调用设计的核心思想:业务方法保持纯净,不感知模型的存在;模型侧的调用意图通过声明式特性暴露。这样既方便单元测试,也能让工具方法在非 AI 场景下继续使用。
4.3 与现有系统的磨合点
把 DSH-Sharp 接进老项目的过程中,有几个磨合点需要特别留意。
日志接入是最常见的需求。DSH-Sharp 没有绑定某个具体日志框架,而是暴露了一个ILogSink接口,你可以用它把内部日志转发到自己项目的日志体系。默认实现的输出级别是 Information,请求正文和密钥绝对不会出现在日志里。如果你正在用 Serilog 或 NLog 一类的框架,写一个适配器转发即可,改动量很小。
依赖注入也不是强制要求。在 ASP.NET Core 项目里可以注册成单例服务,在上位机项目里直接静态持有客户端实例也可以。我个人的建议是:服务端项目走依赖注入,桌面项目走单例持有,两种方式 DSH-Sharp 都支持。客户端内部对 HttpClient 的管理方式考虑了这两种使用场景,不会因为短生命周期对象频繁创建而耗尽连接。
配置源的对接同样灵活。配置文件不是唯一选项,你可以实现自己的IConfigProvider,从数据库、注册表或者现有配置中心读取配置。我在一个老项目里就是这么干的:AI 相关配置从现有配置库里读,不用为 DSH-Sharp 单独搭建一套配置体系。
5. 实测中的坑和我的处理记录
5.1 HttpClient 生命周期的坑
这是所有 .NET 调接口项目都会遇到的经典坑,DSH-Sharp 早期版本也栽过。最初客户端在每次请求时new HttpClient(),本地并发测试跑了几十个请求之后,日志里开始出现SocketException: Only one usage of each socket address。原因在于频繁创建销毁 HttpClient 会导致底层 socket 不被及时释放,端口被耗尽。
修复方案是在客户端内部持有静态的 HttpClient 实例,并配合SocketsHttpHandler合理配置连接池参数。如果你在自己的项目里手动封装模型调用,也一定要记住这条:HttpClient 应该是长期复用对象,不是一次性的请求工具。在 ASP.NET Core 里用IHttpClientFactory也行,在上位机里用静态实例也可以,关键是不能短生命周期地频繁 new 和释放。
这个坑之所以隐蔽,是因为它不是必现的。三五个并发看不出来,等到业务高峰期并发一上来,问题就像定时炸弹一样爆发。我现在的处理方式是在压测阶段就跑一轮并发冒烟,不要等到生产环境出问题再排查。
5.2 SSE 半包解析的边界
SSE 解析器是我调试时间最长的一个模块。表面看格式很简单,一段数据以空行分隔,但真实网络环境下会遇到各种诡异的切割:一个完整事件被拆成三个包到达,或者两个事件被 TCP 粘包合并成一次读取。
第一个版本的解析器老老实实按行读取,结果流式输出经常丢字或者卡死。后来改成字节流缓冲模型:底层逐块读取字节,内部用一个可增长缓冲数组积累数据,再按事件边界切割。只有当缓冲区里出现了完整的\n\n分隔符时,才解析出一个完整事件;否则继续等待下一块数据。这个改动彻底解决了流式输出的稳定性问题。
现在的实现对外是透明的,调用方只感知IAsyncEnumerable。但如果你自己写封装,这里一定要做缓冲处理,不要假定每次ReadAsync返回的就是完整事件。这是流式解析最核心的边界条件。
5.3 JSON 序列化策略不一致
System.Text.Json 默认的命名策略是 camelCase,属性名首字母小写;而某些模型服务的响应字段命名策略不一定相同。前期我没有统一配置序列化选项,结果在对接不同服务时,有的字段解析出来是 null,排查了半天才发现是命名策略不匹配。
DSH-Sharp 的处理方式是把序列化策略收敛到一处:所有内部请求和响应解析都显式设置JsonSerializerOptions,并且在工具调用的 Schema 生成、参数反序列化里使用同一套配置。这样无论对接哪个服务,只需要调整一个地方,就能全局生效。
如果你用的是其他 C# 封装库,我建议动手写代码之前先确认一下服务端的 JSON 命名策略,并且把所有解析逻辑统一收口。最怕的就是有的地方用 camelCase、有的地方用 PascalCase,调试起来非常痛苦。
5.4 并发窗口与流控
我说过早期版本不加并发限制,压测时 50 个并发请求同时打出去,结果大量请求超时。问题根源有两个方面:一是本地机器到服务端的连接数过多,二是服务端对单客户端有速率限制,触发限流后原本健康的请求也被迫排队。
修复方式是引入SemaphoreSlim作为信号量窗口,默认允许最多 10 个请求同时进行。超出部分进入等待队列,等有请求完成释放信号量后再继续。这个窗口大小可以通过配置调整,实测下来 10 到 20 之间比较合理。如果业务确实需要更高的并发,建议在配置里同时调大 HTTP 连接池上限,避免客户端侧的瓶颈转移到传输层。
流控阈值不是越大越好,关键看服务端限制和业务延迟的平衡点。宁可让请求排队多等几百毫秒,也不要让线程池被大量超时任务占满。这条经验同样适用于任何 C# 服务调接口的场景。
6. 当前边界与后续规划
6.1 当前版本不做什么
发布说明里把边界说清楚,是对使用者负责。当前版本的 DSH-Sharp 明确不做以下几件事:
- 不做本地模型推理。它定位是模型服务的客户端工具集,模型本身跑在远端或专用服务上,DSH-Sharp 不负责加载模型权重,也不提供推理后端。
- 不做 UI 组件。对话气泡、输入框、流式打字机效果这类 UI 问题是领域层的事情,DSH-Sharp 只负责数据层的能力,展示层由开发者结合自己的框架实现。
- 不支持老版本 .NET Framework。目标框架是 .NET 8 及以上。这保证了语言特性和底层 API 的可用性,也避免了兼容老框架带来的大量条件编译。存量老系统如果暂时无法升级目标框架,可能要用其他方式做中转。
- 不做全流程全家桶。没有向量数据库、没有内存知识库、没有自动化 Agent 框架。这些是上层应用,DSH-Sharp 提供的是接入基础设施。
这些边界是经过权衡后主动划定的,而不是没来得及做。明确不做的事,反而能让核心功能更可靠。
6.2 后续打算做什么
接下里的迭代方向主要有四个,按优先级排列:
- 语义缓存。对于重复度高的请求,例如"介绍一下这个系统",可以在本地缓存向量化后的结果,命中缓存时直接返回,减少模型调用成本。这需要在请求层做相似度比较,属于纯基础设施能力。
- 更细粒度的可观测性。当前版本的诊断接口能记录调用耗时、状态码、延迟分布,后续希望补充链路追踪字段,与内部监控系统打通,方便定位每一个请求在哪个环节耗时最高。
- AOT 发布支持。C# 的 Native AOT 发布在桌面工具场景中越来越常见,编译期需要可裁剪的代码路径,这意味着反射扫描要做一定的调整,目前正在验证。
- 多协议适配。现在优先支持 Chat 类模型服务的协议,后续打算抽象出协议适配层,让同一套工具框架对接不同的模型服务,而不影响上层调用代码。
这些规划都围绕一个原则:保持核心边界清晰,扩展能力用插件式设计实现,不把核心库变成什么都装的杂货铺。
6.3 我的一点体会
做完这个项目,最大的体会是:模型接入这件事,难点从来不在"调通接口",而在接口之外的工程细节。重试策略怎么定、流式数据怎么解析、工具调用怎么循环、日志怎么打才不泄漏敏感信息——这些才是决定一个封装真正好不好用的关键。DSH-Sharp 把这套工程经验沉淀成了可复用的代码,这也是我建议你在自己的项目里不要重复造轮子的原因。与其每次接入模型服务都从头踩一遍连接池、半包、序列化这些坑,不如直接站在已经处理过这些问题的肩膀上。
最后分享一个使用建议:初次接入时,先用本地模拟运行时把对话和工具调用的链路整体跑通,再切换到真实模型服务。这样开发调试时完全不依赖网络和配额,等代码稳定后再做真实联调,效率会高很多。工具调用的正确性检查,也可以先用模拟返回值做断言,确保循环逻辑和参数绑定无误,再验证真实模型的意图识别效果,排错会轻松许多。