整体设计思路
- 单一职责原则的体现
这个DoubaoService类遵循了单一职责原则,专门负责与火山方舟 API 的交互。它将所有相关的功能封装在一个类中,包括:
-
图片推理(使用视觉模型)
-
PDF 推理(使用 Bot 应用)
-
文本推理(使用自定义接入点)
这样的设计使得代码结构清晰,职责明确,便于维护和扩展。
- 配置参数的集中管理
csharp
#region 配置参数(建议从配置文件读取API Key)
private readonly string _apiKey = "秘钥";
private readonly string _botApiUrl = "https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions";
private readonly string _botAppId = "应用ID";
private readonly string _llmApiUrl = "https://ark.cn-beijing.volces.com/api/v3/chat/completions";
private readonly Dictionary<string, string> _modelMap = new Dictionary<string, string>
{
{ "image", "doubao-seed-1-6-251015" }, // 视觉模型
{ "text", "自定义推理接入点的ID" } // 自定义接入点(文本)
};
private readonly string _linkReaderToolUrl = "https://ark.cn-beijing.volces.com/api/v3/tools/link_reader/invoke";
#endregion
设计原因:
-
集中管理所有 API 相关的配置参数
-
便于统一修改和维护
-
提供了清晰的配置文档(注释说明)
-
建议从配置文件读取 API Key,增强安全性
- 方法分组和功能模块化
类中的方法按照功能进行了清晰的分组:
-
图片推理相关方法
-
PDF 推理相关方法
-
文本推理相关方法
-
核心 API 调用方法
-
资源管理方法
这种模块化设计使得代码易于理解和维护。
为什么要实现 IDisposable 接口?
- 非托管资源的管理需求
这个类使用了HttpClient,这是一个实现了IDisposable接口的对象。虽然在现代.NET 中,HttpClient通常建议使用单例模式或通过IHttpClientFactory来管理,但在这个特定的设计中,为了保证每个服务实例的独立性和资源隔离,选择了在类内部管理HttpClient的生命周期。
- 确定性资源释放
实现IDisposable接口的主要目的是提供确定性的资源释放机制。当类的实例不再需要时,调用方可以显式调用Dispose()方法来立即释放资源,而不是等待垃圾回收器自动回收。
- 避免资源泄漏
如果不实现IDisposable接口,HttpClient使用的网络连接、套接字等资源可能不会被及时释放,导致:
-
连接池耗尽
-
系统句柄泄漏
-
性能下降
-
甚至应用程序崩溃
Dispose 模式的实现解析
标准 Dispose 模式的实现
csharp
#region IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing) _httpClient?.Dispose();
_disposed = true;
}
#endregion
设计原因详解
- 双 Dispose 方法设计
-
public void Dispose():供调用方显式调用的公共方法
-
protected virtual void Dispose(bool disposing):实际执行资源释放的受保护方法
为什么这样设计?
-
允许子类重写 Dispose 逻辑
-
区分托管资源和非托管资源的释放
-
避免重复释放资源
- _disposed标志的作用
private bool _disposed = false;
设计原因:
-
防止重复释放资源
-
线程安全的基本保障
-
确保 Dispose 逻辑只执行一次
- GC.SuppressFinalize(this)的作用
设计原因:
-
告诉垃圾回收器不需要调用终结器
-
提高性能,减少垃圾回收的工作量
-
遵循标准的 Dispose 模式最佳实践
- 条件释放逻辑
if (disposing) _httpClient?.Dispose();
设计原因:
-
disposing参数为 true 时,释放托管资源
-
?.空合并运算符确保安全调用
-
遵循 "只在 disposing 为 true 时释放托管资源" 的最佳实践
HttpClient 管理策略的设计考量
HttpClient 的特殊性质
HttpClient在.NET 中有一些特殊的性质:
-
它是线程安全的
-
它的创建成本较高
-
它会管理连接池
本设计中的 HttpClient 管理
csharp
private readonly HttpClient _httpClient;
public DoubaoService()
{
var handler = new HttpClientHandler
{
AllowAutoRedirect = true,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = false,
// 开发环境添加(生产环境删除)
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
};
_httpClient = new HttpClient(handler);
_httpClient.Timeout = TimeSpan.FromMinutes(15);
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
}
设计原因:
- 每个服务实例一个 HttpClient
-
确保每个服务实例的配置独立性
-
避免不同实例之间的配置冲突
-
便于实现细粒度的资源管理
- 自定义 HttpClientHandler 配置
-
AllowAutoRedirect = true:支持 HTTP 重定向
-
AutomaticDecompression:自动处理 Gzip 和 Deflate 压缩
-
UseCookies = false:禁用 Cookie,提高安全性
-
证书验证回调:开发环境下方便调试
- 超时设置
_httpClient.Timeout = TimeSpan.FromMinutes(15);
设计原因:
-
火山方舟 API 可能需要较长的处理时间(特别是图片和 PDF 处理)
-
避免因超时导致的请求失败
-
15 分钟的超时时间足够处理大多数场景
- 默认请求头配置
csharp
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
设计原因:
-
避免在每个请求中重复设置相同的头信息
-
提高代码的简洁性和可维护性
-
确保认证信息的统一管理
其他关键设计决策解析
- 异步方法设计
所有的 API 调用方法都是异步的:
csharp
public async Task<AiResponse> VerifyImagesAsync(string systemPrompt, List<string> imageUrls, Dictionary<string, string> targetValues)
public async Task<AiResponse> VerifyPDFBotAsync(string pdfUrl, string analysisRequirements, Dictionary<string, string> targetValues = null)
public async Task<AiResponse> VerifyTextAsync(string systemPrompt, string textContent, Dictionary<string, string> targetValues = null)
设计原因:
-
网络 I/O 操作适合异步处理
-
提高应用程序的响应性
-
充分利用系统资源
-
符合现代.NET 开发的最佳实践
- 输入参数验证
csharp
if (imageUrls == null || !imageUrls.Any())
throw new ArgumentException("图片URL列表不能为空");
if (string.IsNullOrWhiteSpace(systemPrompt))
throw new ArgumentException("验证规则不能为空")
设计原因:
-
尽早发现错误
-
提供清晰的错误信息
-
防止无效参数导致的后续问题
-
提高代码的健壮性
- JSON 序列化配置
csharp
string requestJson = JsonConvert.SerializeObject(requestBody, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.None
});
设计原因:
-
NullValueHandling = NullValueHandling.Ignore:减少不必要的网络传输
-
Formatting = Formatting.None:提高性能,减少数据量
-
符合 API 的要求
- 响应处理和错误处理
csharp
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new Exception($"Bot API调用失败(状态码:{response.StatusCode}):{errorContent}");
}
设计原因:
-
详细的错误信息便于调试
-
包含状态码和错误内容
-
遵循异常处理的最佳实践
设计模式的应用
- 封装模式
整个类将复杂的 API 调用逻辑封装起来,对外提供简单易用的接口。
- 资源管理模式
实现了标准的 Dispose 模式,确保资源的正确管理。
- 策略模式
通过_modelMap字典实现了不同模型的策略选择:
private readonly Dictionary<string, string> _modelMap = new Dictionary<string, string>
{
{ "image", "doubao-seed-1-6-251015" }, // 视觉模型
{ "text", "自定义推理接入点的ID" } // 自定义接入点(文本)
};
安全性考量
- API Key 的管理
代码中建议从配置文件读取 API Key,而不是硬编码:
#region 配置参数(建议从配置文件读取API Key)
private readonly string _apiKey = "秘钥";
#endregion
- 证书验证
在开发环境中禁用了证书验证,但建议在生产环境中删除:
// 开发环境添加(生产环境删除)
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
性能优化
- 连接池管理
通过HttpClient的连接池管理提高性能。
- 压缩支持
启用了 Gzip 和 Deflate 压缩:
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
- 超时设置
合理的超时设置避免资源浪费。
DoubaoService 代码
csharp
using CAS.Model.Model;
using CAS.Model.TianYanCha;
using log4net.Repository.Hierarchy;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
public class DoubaoService : IDisposable
{
#region 配置参数(建议从配置文件读取API Key)
private readonly string _apiKey = "秘钥";
// Bot应用配置(用于PDF和文本)
private readonly string _botApiUrl = "https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions";
private readonly string _botAppId = "应用ID";
private readonly string _llmApiUrl = "https://ark.cn-beijing.volces.com/api/v3/chat/completions";
private readonly Dictionary<string, string> _modelMap = new Dictionary<string, string>
{
{ "image", "doubao-seed-1-6-251015" }, // 视觉模型
{ "text", "自定义推理接入点的ID" } // 自定义接入点(文本)
};
// 网页解析工具URL
private readonly string _linkReaderToolUrl = "https://ark.cn-beijing.volces.com/api/v3/tools/link_reader/invoke";
private readonly HttpClient _httpClient;
private bool _disposed = false;
#endregion
public DoubaoService()
{
var handler = new HttpClientHandler
{
AllowAutoRedirect = true,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = false,
// 开发环境添加(生产环境删除)
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
};
_httpClient = new HttpClient(handler);
_httpClient.Timeout = TimeSpan.FromMinutes(15);
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
}
#region 1. 图片推理(使用自定义推理接入点+视觉模型)
public async Task<AiResponse> VerifyImagesAsync(string systemPrompt, List<string> imageUrls, Dictionary<string, string> targetValues)
{
if (imageUrls == null || !imageUrls.Any())
throw new ArgumentException("图片URL列表不能为空");
if (string.IsNullOrWhiteSpace(systemPrompt))
throw new ArgumentException("验证规则不能为空");
var messages = new List<object>();
// 构建混合内容的USER消息(图片 + 文本)
var userContent = new List<object>();
// 添加图片
foreach (var url in imageUrls)
{
var trimmedUrl = url.Trim();
if (!Uri.IsWellFormedUriString(trimmedUrl, UriKind.Absolute))
throw new ArgumentException($"无效的图片URL:{trimmedUrl}");
userContent.Add(new
{
type = "image_url",
image_url = new { url = trimmedUrl, detail = "high" }
});
}
// 添加验证参数文本
if (targetValues != null && targetValues.Count > 0)
{
var paramText = $"\"验证参数:{string.Join(";", targetValues.Select(kv => $"{kv.Key}:{kv.Value}"))}\"";
userContent.Add(new
{
type = "text",
text = paramText
});
}
// 添加USER消息(包含图片和文本的混合内容)
messages.Add(new
{
role = "user",
content = userContent
});
// 添加SYSTEM消息在最后面
messages.Add(new
{
role = "system",
content = systemPrompt
});
// 调用视觉模型
return await CallLlmApiAsync(_modelMap["image"], messages);
}
#endregion
#region 2. PDF推理(优先使用Bot应用)
public async Task<AiResponse> VerifyPDFBotAsync(string pdfUrl, string analysisRequirements, Dictionary<string, string> targetValues = null)
{
if (!Uri.IsWellFormedUriString(pdfUrl, UriKind.Absolute))
throw new ArgumentException($"无效的PDF URL:{pdfUrl}");
if (string.IsNullOrWhiteSpace(analysisRequirements))
throw new ArgumentException("分析要求不能为空");
var messages = new List<object>();
// 构建消息内容
var content = new List<object>();
if (targetValues != null && targetValues.Count > 0)
{
var paramText = $"验证参数:{string.Join(";", targetValues.Select(kv => $"{kv.Key}:{kv.Value}"))}";
content.Add(new { type = "text", text = paramText });
}
content.Add(new { type = "text", text = $"请分析以下PDF文件:{pdfUrl}\n\n分析要求:{analysisRequirements}" });
messages.Add(new
{
role = "user",
content = content
});
// 添加SYSTEM消息在最后
messages.Add(new
{
role = "system",
content = analysisRequirements
});
return await CallBotApiAsync(messages);
}
#endregion
#region 3. 文本推理
public async Task<AiResponse> VerifyTextAsync(string systemPrompt, string textContent,
Dictionary<string, string> targetValues = null)
{
if (string.IsNullOrEmpty(textContent))
throw new ArgumentException("待分析文本不能为空");
if (string.IsNullOrWhiteSpace(systemPrompt))
throw new ArgumentException("系统提示不能为空");
var messages = new List<object>();
// 1. SYSTEM消息在最前面
messages.Add(new
{
role = "system",
content = $"\"{systemPrompt}\"" // 添加双引号
});
// 2. 待分析文本作为单独的USER消息
messages.Add(new
{
role = "user",
content = $"\" 待分析文本:{textContent} \"" // 添加双引号和空格
});
// 3. 验证参数作为单独的USER消息
if (targetValues != null && targetValues.Count > 0)
{
foreach (var kvp in targetValues)
{
var paramText = $"验证参数:{kvp.Key}:{kvp.Value}";
messages.Add(new
{
role = "user",
content = $"\"{paramText}\"" // 添加双引号
});
}
}
return await CallLlmApiAsync(_modelMap["text"], messages);
}
#endregion
#region 核心API调用方法
/// <summary>
/// 调用Bot应用API
/// </summary>
private async Task<AiResponse> CallBotApiAsync(List<object> messages)
{
try
{
var requestBody = new
{
model = _botAppId,
messages = messages,
temperature = 0.0,
stream = false,
thinking = new { type = "disabled" },
response_format = new { type = "json_object" }
};
string requestJson = JsonConvert.SerializeObject(requestBody, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.None
});
Console.WriteLine($"Bot API请求:{requestJson}");
var httpContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(_botApiUrl, httpContent);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new Exception($"Bot API调用失败(状态码:{response.StatusCode}):{errorContent}");
}
var responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Bot API响应:{responseContent}");
return DeserializeAiResponse(responseContent, "Bot API");
}
catch (Exception ex)
{
throw new Exception($"Bot API处理失败:{ex.Message}", ex);
}
}
/// <summary>
/// 调用LLM推理API(自定义接入点)
/// </summary>
private async Task<AiResponse> CallLlmApiAsync(string modelName, List<object> messages)
{
try
{
var requestBody = new
{
model = modelName,
messages = messages,
temperature = 0.0,
top_p = 0.1,
stream = false,
thinking = new { type = "disabled" },
response_format = new { type = "json_object" }
};
string requestJson = JsonConvert.SerializeObject(requestBody, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.None
});
Console.WriteLine($"LLM API请求:{requestJson}");
var httpContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(_llmApiUrl, httpContent);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new Exception($"LLM API调用失败(状态码:{response.StatusCode}):{errorContent}");
}
var responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"LLM API响应:{responseContent}");
return DeserializeAiResponse(responseContent, "LLM API");
}
catch (Exception ex)
{
throw new Exception($"LLM API处理失败:{ex.Message}", ex);
}
}
private AiResponse DeserializeAiResponse(string responseContent, string apiType)
{
if (string.IsNullOrWhiteSpace(responseContent))
{
throw new Exception($"{apiType}返回空响应");
}
try
{
// 尝试直接反序列化
var aiResponse = JsonConvert.DeserializeObject<AiResponse>(responseContent);
if (aiResponse == null)
{
throw new Exception($"{apiType}响应反序列化为null");
}
if (aiResponse.VerificationResults == null || !aiResponse.VerificationResults.Any())
{
// 检查是否有choices字段(某些API可能返回这种格式)
dynamic dynamicResponse = JsonConvert.DeserializeObject(responseContent);
if (dynamicResponse.choices != null && dynamicResponse.choices.Count > 0)
{
var content = dynamicResponse.choices[0].message.content.ToString();
if (!string.IsNullOrWhiteSpace(content))
{
try
{
// 尝试解析choices中的content
return JsonConvert.DeserializeObject<AiResponse>(content);
}
catch
{
// 如果content不是JSON,直接返回包含原始内容的响应
return new AiResponse
{
VerificationResults = new List<AiVerificationItem>
{
new AiVerificationItem
{
Parameter = "响应内容",
Result = "解析警告",
Reason = $"原始响应:{content}"
}
}
};
}
}
}
throw new Exception($"{apiType}响应中未包含有效的验证结果");
}
return aiResponse;
}
catch (Exception ex)
{
throw new Exception($"{apiType}响应反序列化失败:{ex.Message},响应内容:{responseContent}");
}
}
#endregion
#region IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing) _httpClient?.Dispose();
_disposed = true;
}
#endregion
#region 数据模型类
public class Message
{
[JsonProperty("role")]
public string Role { get; set; }
[JsonProperty("content")]
public string Content { get; set; }
}
public class LinkReaderParseResult
{
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("content")]
public string Content { get; set; }
}
#endregion
}
总结
这个火山方舟 API 的 C# 服务类体现了良好的软件设计原则和最佳实践:
-
单一职责原则:专注于 API 调用功能
-
资源管理:实现了标准的 Dispose 模式
-
代码组织:清晰的方法分组和模块化设计
-
错误处理:详细的输入验证和错误信息
-
性能优化:异步操作、连接池管理等
-
安全性:API Key 管理、证书验证等