在生产环境中运行
默认设置针对典型的 SaaS 客户进行了调优。下面是流量形态变化时需要调整的参数——速率限制、重试策略、审计日志、依赖可达性探针、IHealthCheck 集成、幂等性、提示词模板覆盖,以及 BYOK 管理接口。
AI 调用的速率限制
POST /richtextbox/ai 和 POST /richtextbox/ai/stream 共用一个按 IP 计算的令牌桶,这样单个失控的标签页就无法在几秒钟内耗尽您的服务商配额。默认值:每个 IP 每分钟 30 次调用。
builder.Services.AddRichTextBox(opts => { opts.AiRateLimit = 30; // calls per window opts.AiRateLimitWindow = TimeSpan.FromMinutes(1); // window length });
设置 AiRateLimit = 0 可禁用限制,也可以换成基于 Redis 的限流器:
builder.Services.AddSingleton<IRichTextBoxAiRateLimiter, RedisAiRateLimiter>(); builder.Services.AddRichTextBox();
服务商临时故障时的 HTTP 重试
OpenAI / Anthropic / Azure OpenAI 在正常负载下也会返回 429 和 503。内置的解析器会对这些状态码(以及 502 / 504 和连接中断)进行重试,采用带完全抖动的指数退避,上限为 30 秒。会遵循服务商的 Retry-After 标头。
services.AddRichTextBoxOpenAiResolver(opts => { opts.ApiKey = builder.Configuration["OpenAI:ApiKey"]; opts.MaxRetryAttempts = 2; // 0 to disable opts.RetryBaseDelay = TimeSpan.FromMilliseconds(500); });
幂等键
每个 POST /richtextbox/ai、/ai/stream 和 /upload 都会遵循 Idempotency-Key。重复的重试会原样重放缓存的响应体——不会重复计费,也不会产生孤立文件。流式响应会逐字节重放整个 SSE 字节流。
builder.Services.AddRichTextBox(opts => { opts.IdempotencyTtl = TimeSpan.FromHours(1); opts.IdempotencyPruneInterval = TimeSpan.FromMinutes(5); });
多实例主机:请注册一个基于 Redis 的 IIdempotencyStore,代替默认的内存存储。借助原生 TTL,您可以禁用定期清理程序。
安全审计日志
每一次与安全相关的拒绝,都会在 RichTextBox.Audit 类别下输出一条结构化的 LogWarning,并带有稳定的 EventId:
| EventId | 名称 | 触发时机 |
|---|---|---|
| 8001 | LicenseInvalid | 端点因 .lic 文件缺失或无效而拒绝了请求。 |
| 8101 | UploadMagicByteMismatch | 文件签名与声明的扩展名不符。 |
| 8102 | UploadRejectedByValidator | 某个 IUploadValidator 返回了 Reject。 |
| 8103 | UploadInvalidExtension | 上传因不在允许列表中而被拒绝。 |
| 8104 | UploadOversize | 超过了 MaxUploadBytes。 |
| 8201 | AiRequestOversize | AI 请求体超过了 MaxRequestBytes。 |
| 8202 | AiRateLimitExceeded | 调用方超过了按 IP 计算的 AI 速率限制。 |
| 8203 | AiResolverException | AI 解析器抛出了异常;记录时附带关联 id。 |
| 8204 | AiKeyVaultMiss | BYOK 密钥库没有返回密钥。 |
| 8205 | AiKeyVaultHit | BYOK 密钥库返回了密钥(记录 KeyId,绝不记录密钥本身)。 |
| 8206 | AiResponseFilterMatched | 某个 IRichTextBoxAiResponseFilter 修改了响应。 |
把审计事件路由到单独的流
IRichTextBoxAuditSink 让受合规监管的工作负载(HIPAA / PCI / SOC2)只把审计事件路由到一次写入的目标——Splunk HEC、启用了对象锁定的 S3、专用于审计的数据库。附带的 JsonLinesAuditSink 适用于离线/单实例场景:
builder.Services.AddRichTextBox(); builder.Services.AddJsonLinesAuditSink("/var/log/myapp/rtb-audit.jsonl");
Channel 加后台排空程序;RecordAsync 从不阻塞。队列溢出时会丢弃事件,并记录 DroppedCount 指标。可以自然地注册多个接收器——把 JsonLines 与实时的 Splunk 接收器搭配使用,无需额外成本。依赖可达性探针
可选的配套接口让任何 RichTextBox 依赖都能报告其后端服务是否可达。/richtextbox/health 端点会把结果与授权状态一起显示:
| 接口 | 内置实现 | 探测目标 |
|---|---|---|
IRichTextBoxAiResolverProbe | OpenAI、Azure OpenAI(免费);AnthropicResolverWithProbe 包装器(消耗少量令牌的 POST) | GET /v1/models / GET /openai/deployments / 带 max_tokens=1 的 POST /v1/messages |
IIdempotencyStoreProbe | 自定义的 Redis / SQL 存储(由您实现) | PING / SELECT 1 |
IRichTextBoxUploadStoreProbe | 自定义的 S3 / Azure Blob / Cloudinary 存储 | 对存储桶执行 HEAD / list-objects --max-items 1 |
builder.Services.AddRichTextBox(opts => { opts.AiResolverHealthProbeEnabled = true; opts.AiResolverHealthProbeTimeout = TimeSpan.FromSeconds(3); opts.AiResolverHealthProbeCacheTtl = TimeSpan.FromSeconds(30); opts.IdempotencyStoreHealthProbeEnabled = true; opts.UploadStoreHealthProbeEnabled = true; });
每个探针上的缓存 TTL 可以防止负载均衡器的频繁探测——否则每 5 秒请求一次 /health 的负载均衡器每分钟会产生 12 次探测,即使探测会产生真实费用也是如此。
标准的 IHealthCheck 集成
对于使用 Microsoft.Extensions.Diagnostics.HealthChecks 的主机,注册适配器之后,RichTextBox 的状态就会与您的其他检查一起显示——无需另建一套监控路径:
builder.Services.AddRichTextBox(); builder.Services.AddHealthChecks() .AddRichTextBox(name: "richtextbox", failureStatus: HealthStatus.Unhealthy, tags: new[] { "ready" } ); app.MapHealthChecks("/health");
授权和所有受控探针都通过时返回 Healthy;否则返回 Unhealthy,并附带指明失败依赖的描述。HealthCheckResult.Data 与 /richtextbox/health 的 JSON 载荷相同。
BYOK——按租户分配的 API 密钥
多租户 SaaS 主机可以通过 IAiKeyVault + IAiKeyVaultAdmin,把 AI 流量计入每个租户自己的服务商账户。软件包附带一个内存中的参考实现;生产部署则以 Azure Key Vault、Redis 或真正的 SQL 存储作为后端。
完整的约定、管理 REST 端点和参考实现(Redis、Azure Key Vault、磁盘加密的 FileBackedAiKeyVault),请参阅专门的 BYOK 密钥库页面。
按调用的成本台账
IRichTextBoxAiCostSink 为每次 AI 调用输出一条 AiUsageRecord——服务商、模型、模式、令牌数(输入/输出/总计)、延迟、状态、KeyId。可以转发到您的计费系统,用于向租户分摊费用:
builder.Services.AddSingleton<IRichTextBoxAiCostSink, MyBillingApiCostSink>();对于高流量租户,请用缓冲接收器进行包装,这样内部接收器在每个时间间隔内只看到一次批量刷新,而不是每个 AI 请求一次 HTTP/SQL 调用。如果您的内部接收器还实现了 IRichTextBoxAiCostBatchSink,包装器会通过 RecordBatchAsync 路由——执行一次 INSERT,而不是 N 次。
builder.Services.AddBufferingAiCostSink<MyBillingApiCostSink>( flushInterval: TimeSpan.FromSeconds(30), maxBatchSize: 500);
输出过滤——PII 脱敏、内容拦截
IRichTextBoxAiResponseFilter 在解析器之后、响应发出之前运行。参考实现 RegexAiResponseFilter 可以覆盖约 80% 的实际 PII 脱敏需求:
var emailMask = new Regex(@"[\w.+-]+@[\w-]+\.[\w.-]+"); var ssnMask = new Regex(@"\b\d{3}-\d{2}-\d{4}\b"); builder.Services.AddSingleton<IRichTextBoxAiResponseFilter>( new RegexAiResponseFilter( (emailMask, "[email-redacted]"), (ssnMask, "[ssn-redacted]")));
对于流式响应,注册了过滤器时,框架会在服务器端缓冲令牌(默认 BufferStreamingForFilters = true),这样跨越多个增量片段的信用卡号正则表达式也能真正匹配。
提示词模板覆盖
IRichTextBoxPromptTemplateProvider 允许您覆盖内置解析器按模式发送的系统提示词。使用装饰器基类 PromptTemplateProviderBase,只覆盖您需要的方法即可:
public sealed class FormalTonePromptProvider : PromptTemplateProviderBase { public override string BuildSystemPrompt(RichTextBoxAiRequest request, string? operatorSuffix) => base.BuildSystemPrompt(request, operatorSuffix) + "\n\nAlways reply in formal British English; avoid contractions."; }
如果只是临时调整某个模式而不想实现完整的提供程序,可以使用字典:
services.AddRichTextBoxOpenAiResolver(opts => { opts.ApiKey = "sk-..."; opts.SystemPromptOverridesByMode["proofread"] = "You are a hyper-pedantic copy-editor for legal documents."; });
提示词注入筛查
IRichTextBoxAiPolicy 是 AI 端点在调用解析器之前要经过的关卡。内置的 DefaultPromptInjectionPolicy 会拒绝现成的越狱模板("ignore previous instructions"、"you are now DAN" 等)。可以叠加更多策略,用于 PII 脱敏或按租户限定的模式允许列表。
builder.Services.AddSingleton<IRichTextBoxAiPolicy, DefaultPromptInjectionPolicy>(); builder.Services.AddSingleton<IRichTextBoxAiPolicy, MyTenantPolicy>();
分布式跟踪(OpenTelemetry)
每次 AI 调用、DOCX 导出和上传,都会通过 RichTextBox.AspNetCore ActivitySource 输出一个 span。请在您的 tracer provider 配置中订阅它:
builder.Services.AddOpenTelemetry().WithTracing(t => t .AddSource(RichTextBoxDiagnostics.SourceName) // "RichTextBox.AspNetCore" .AddAspNetCoreInstrumentation() .AddOtlpExporter());
启动时的选项验证
RichTextBoxOptionsValidator + AiResolverOptionsValidator<T> 会在第一次解析 IOptions.Value 时运行——在主机接受第一个请求之前。错误的配置(遗漏了 /、字节上限为负数、心跳间隔 > 超时时间、缺少 API 密钥、缺少 Azure 部署)会在启动时抛出 OptionsValidationException,让部署明确地失败。
无需任何连接代码。验证器会通过 AddRichTextBox() 自动注册。