Final Project / 03
Query Engine 实现
Query Engine 是 Harness 的第一个要做稳的模块。原因很简单:Agent 的每一步决策都要过它,如果这层不稳,上面所有逻辑都是空中楼阁。
很多人写 Agent 把模型调用当成一行代码:
const result = await anthropic.messages.create({ ... });
这对 Demo 没问题。但生产环境你会遇到:
- 模型 API 偶发 500,需要重试
- 请求太频繁被限流,需要排队
- 流式输出中途断开,需要优雅恢复
- 相同问题反复问,需要缓存
- 不同任务适合不同模型,需要路由
- Token 用超了,需要预判和截断
Query Engine 就是把这些问题收敛到一层,让上层只关心“发消息、收回复”。
模块结构
src/query-engine/
├── index.ts # 统一出口
├── engine.ts # QueryEngine 主类
├── provider.ts # Provider 接口定义
├── providers/
│ ├── claude.ts # Anthropic 实现
│ ├── openai.ts # OpenAI 实现
│ └── deepseek.ts # DeepSeek 实现(继承 OpenAI)
├── stream.ts # 流式响应解析器
├── retry.ts # 重试策略
├── cache.ts # 语义缓存
├── rate-limiter.ts # 令牌桶限流
├── router.ts # 模型路由
├── token-counter.ts # Token 计数与预算
└── errors.ts # 统一错误分类
Provider 接口:统一三家模型
三家模型 API 的差异不小(Anthropic 有自己的格式,OpenAI 和 DeepSeek 共用 OpenAI 格式但细节有别),Provider 层把这些差异屏蔽掉。
// provider.ts
export interface StreamParams {
model: string;
messages: Message[];
tools?: ToolSchema[];
maxTokens?: number;
temperature?: number;
systemPrompt?: string;
abortSignal?: AbortSignal;
}
export interface TokenUsage {
inputTokens: number;
outputTokens: number;
cacheReadTokens?: number;
cacheWriteTokens?: number;
}
export type StreamEvent =
| { type: 'text_delta'; content: string }
| { type: 'tool_use_start'; id: string; name: string }
| { type: 'tool_use_delta'; input: string }
| { type: 'tool_use_end' }
| { type: 'message_end'; usage: TokenUsage; stopReason: StopReason };
export type StopReason = 'end_turn' | 'tool_use' | 'max_tokens';
export interface LLMProvider {
name: string;
stream(params: StreamParams): AsyncIterable<StreamEvent>;
countTokens(messages: Message[], tools?: ToolSchema[]): Promise<number>;
}
Claude Provider 实现
Anthropic SDK 有独立的消息格式和 tool calling 协议,需要做转换。
// providers/claude.ts
import Anthropic from '@anthropic-ai/sdk';
import { LLMProvider, StreamParams, StreamEvent } from '../provider';
export class ClaudeProvider implements LLMProvider {
name = 'claude';
private client: Anthropic;
constructor(apiKey: string) {
this.client = new Anthropic({ apiKey });
}
async *stream(params: StreamParams): AsyncIterable<StreamEvent> {
const stream = this.client.messages.stream({
model: params.model,
max_tokens: params.maxTokens ?? 4096,
temperature: params.temperature ?? 0,
system: params.systemPrompt,
messages: this.toAnthropicMessages(params.messages),
tools: params.tools ? this.toAnthropicTools(params.tools) : undefined,
});
for await (const event of stream) {
switch (event.type) {
case 'content_block_start':
if (event.content_block.type === 'tool_use') {
yield {
type: 'tool_use_start',
id: event.content_block.id,
name: event.content_block.name,
};
}
break;
case 'content_block_delta':
if (event.delta.type === 'text_delta') {
yield { type: 'text_delta', content: event.delta.text };
} else if (event.delta.type === 'input_json_delta') {
yield { type: 'tool_use_delta', input: event.delta.partial_json };
}
break;
case 'content_block_stop':
// 判断是否是 tool_use block 结束
yield { type: 'tool_use_end' };
break;
case 'message_stop':
const finalMessage = await stream.finalMessage();
yield {
type: 'message_end',
usage: {
inputTokens: finalMessage.usage.input_tokens,
outputTokens: finalMessage.usage.output_tokens,
cacheReadTokens: finalMessage.usage.cache_read_input_tokens,
cacheWriteTokens: finalMessage.usage.cache_creation_input_tokens,
},
stopReason: finalMessage.stop_reason as StopReason,
};
break;
}
}
}
async countTokens(messages: Message[], tools?: ToolSchema[]): Promise<number> {
const result = await this.client.messages.countTokens({
model: 'claude-sonnet-4-20250514',
messages: this.toAnthropicMessages(messages),
tools: tools ? this.toAnthropicTools(tools) : undefined,
});
return result.input_tokens;
}
private toAnthropicMessages(messages: Message[]): Anthropic.MessageParam[] {
// 转换通用 Message 格式到 Anthropic 格式
// 处理 tool_result 消息的嵌套结构
}
private toAnthropicTools(tools: ToolSchema[]): Anthropic.Tool[] {
return tools.map(t => ({
name: t.name,
description: t.description,
input_schema: t.parameters,
}));
}
}
OpenAI Provider 实现
OpenAI 和 DeepSeek 共用 OpenAI SDK,但 base URL 不同。
// providers/openai.ts
import OpenAI from 'openai';
import { LLMProvider, StreamParams, StreamEvent } from '../provider';
export class OpenAIProvider implements LLMProvider {
name = 'openai';
protected client: OpenAI;
constructor(apiKey: string, baseURL?: string) {
this.client = new OpenAI({ apiKey, baseURL });
}
async *stream(params: StreamParams): AsyncIterable<StreamEvent> {
const messages = this.toOpenAIMessages(params);
const stream = await this.client.chat.completions.create({
model: params.model,
messages,
tools: params.tools ? this.toOpenAITools(params.tools) : undefined,
max_tokens: params.maxTokens ?? 4096,
temperature: params.temperature ?? 0,
stream: true,
});
let currentToolCallId = '';
let currentToolName = '';
let inputTokens = 0;
let outputTokens = 0;
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta;
if (!delta) continue;
// 文本输出
if (delta.content) {
yield { type: 'text_delta', content: delta.content };
}
// Tool call
if (delta.tool_calls) {
for (const tc of delta.tool_calls) {
if (tc.id) {
currentToolCallId = tc.id;
currentToolName = tc.function?.name ?? '';
yield { type: 'tool_use_start', id: currentToolCallId, name: currentToolName };
}
if (tc.function?.arguments) {
yield { type: 'tool_use_delta', input: tc.function.arguments };
}
}
}
// 结束
if (chunk.choices[0]?.finish_reason) {
if (currentToolCallId) {
yield { type: 'tool_use_end' };
}
if (chunk.usage) {
inputTokens = chunk.usage.prompt_tokens;
outputTokens = chunk.usage.completion_tokens;
}
yield {
type: 'message_end',
usage: { inputTokens, outputTokens },
stopReason: this.mapStopReason(chunk.choices[0].finish_reason),
};
}
}
}
async countTokens(messages: Message[]): Promise<number> {
// OpenAI 没有官方 count API,用 tiktoken 本地估算
// 或简单按 4 chars ≈ 1 token 粗估
const text = JSON.stringify(messages);
return Math.ceil(text.length / 4);
}
private toOpenAIMessages(params: StreamParams): OpenAI.ChatCompletionMessageParam[] {
const msgs: OpenAI.ChatCompletionMessageParam[] = [];
if (params.systemPrompt) {
msgs.push({ role: 'system', content: params.systemPrompt });
}
// 转换通用 Message → OpenAI 格式
// 处理 tool_result → tool role 的映射
return msgs;
}
private toOpenAITools(tools: ToolSchema[]): OpenAI.ChatCompletionTool[] {
return tools.map(t => ({
type: 'function',
function: {
name: t.name,
description: t.description,
parameters: t.parameters,
},
}));
}
private mapStopReason(reason: string): StopReason {
if (reason === 'tool_calls') return 'tool_use';
if (reason === 'length') return 'max_tokens';
return 'end_turn';
}
}
DeepSeek Provider:继承 OpenAI
DeepSeek 兼容 OpenAI 接口,只需换 base URL 和模型名。
// providers/deepseek.ts
import { OpenAIProvider } from './openai';
export class DeepSeekProvider extends OpenAIProvider {
name = 'deepseek';
constructor(apiKey: string) {
super(apiKey, 'https://api.deepseek.com');
}
}
// 使用时:
// const ds = new DeepSeekProvider(process.env.DEEPSEEK_API_KEY);
// ds.stream({ model: 'deepseek-chat', messages: [...] });
这就是 OpenAI 兼容接口的好处——DeepSeek 的 Provider 只有 8 行代码。未来接入 Qwen、GLM 等国产模型同理。
Stream Parser:把流式事件组装成结构化响应
Provider 产出的是一个个 StreamEvent,Agent Loop 需要的是完整的结构化响应。Stream Parser 负责这个组装。
// stream.ts
export interface ParsedResponse {
type: 'text' | 'tool_use';
content?: string;
toolCalls?: ToolCall[];
usage: TokenUsage;
stopReason: StopReason;
}
export interface ToolCall {
id: string;
name: string;
input: Record<string, unknown>;
}
export async function parseStream(
events: AsyncIterable<StreamEvent>,
onTextDelta?: (text: string) => void,
): Promise<ParsedResponse> {
let textContent = '';
const toolCalls: ToolCall[] = [];
let currentToolInput = '';
let currentToolId = '';
let currentToolName = '';
let usage: TokenUsage = { inputTokens: 0, outputTokens: 0 };
let stopReason: StopReason = 'end_turn';
for await (const event of events) {
switch (event.type) {
case 'text_delta':
textContent += event.content;
onTextDelta?.(event.content);
break;
case 'tool_use_start':
currentToolId = event.id;
currentToolName = event.name;
currentToolInput = '';
break;
case 'tool_use_delta':
currentToolInput += event.input;
break;
case 'tool_use_end':
toolCalls.push({
id: currentToolId,
name: currentToolName,
input: JSON.parse(currentToolInput),
});
break;
case 'message_end':
usage = event.usage;
stopReason = event.stopReason;
break;
}
}
return {
type: toolCalls.length > 0 ? 'tool_use' : 'text',
content: textContent || undefined,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
usage,
stopReason,
};
}
Retry:分类错误 + 指数退避
不是所有错误都该重试。先分类,再决定策略。
// errors.ts
export type ErrorCategory =
| 'rate_limit' // 429,应该等待后重试
| 'overloaded' // 529/503,服务过载,退避重试
| 'timeout' // 请求超时,可重试
| 'network' // 网络错误,可重试
| 'invalid_request' // 400,参数错误,不可重试
| 'auth' // 401/403,鉴权失败,不可重试
| 'context_length' // 上下文超长,需要压缩后重试
| 'unknown'; // 未知错误
export class QueryEngineError extends Error {
constructor(
message: string,
public category: ErrorCategory,
public retryable: boolean,
public retryAfterMs?: number,
) {
super(message);
}
}
export function classifyError(err: unknown): QueryEngineError {
if (err instanceof Anthropic.RateLimitError) {
const retryAfter = parseInt(err.headers?.['retry-after'] ?? '5') * 1000;
return new QueryEngineError('Rate limited', 'rate_limit', true, retryAfter);
}
if (err instanceof Anthropic.APIStatusError) {
if (err.status === 529) return new QueryEngineError('Overloaded', 'overloaded', true, 10000);
if (err.status === 400) return new QueryEngineError(err.message, 'invalid_request', false);
if (err.status === 401) return new QueryEngineError('Auth failed', 'auth', false);
}
if (err instanceof Error && err.message.includes('timeout')) {
return new QueryEngineError('Timeout', 'timeout', true, 3000);
}
return new QueryEngineError(String(err), 'unknown', false);
}
// retry.ts
export interface RetryConfig {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
backoffMultiplier: number;
}
const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxRetries: 3,
baseDelayMs: 1000,
maxDelayMs: 30000,
backoffMultiplier: 2,
};
export async function withRetry<T>(
fn: () => Promise<T>,
config: RetryConfig = DEFAULT_RETRY_CONFIG,
): Promise<T> {
let lastError: QueryEngineError | undefined;
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
const classified = classifyError(err);
if (!classified.retryable || attempt === config.maxRetries) {
throw classified;
}
lastError = classified;
const delay = classified.retryAfterMs ??
Math.min(
config.baseDelayMs * Math.pow(config.backoffMultiplier, attempt),
config.maxDelayMs,
);
await sleep(delay);
}
}
throw lastError;
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
Rate Limiter:令牌桶算法
防止瞬间打满 API 限额,尤其是 Sub-agent 并行诊断时。
// rate-limiter.ts
export class TokenBucketLimiter {
private tokens: number;
private lastRefill: number;
private queue: Array<{ resolve: () => void }> = [];
constructor(
private maxTokens: number, // 桶容量
private refillRate: number, // 每秒补充多少
) {
this.tokens = maxTokens;
this.lastRefill = Date.now();
}
async acquire(): Promise<void> {
this.refill();
if (this.tokens >= 1) {
this.tokens -= 1;
return;
}
// 桶空了,排队等待
return new Promise(resolve => {
this.queue.push({ resolve });
setTimeout(() => this.processQueue(), 1000 / this.refillRate);
});
}
private refill(): void {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
this.lastRefill = now;
}
private processQueue(): void {
this.refill();
while (this.tokens >= 1 && this.queue.length > 0) {
this.tokens -= 1;
this.queue.shift()!.resolve();
}
}
}
Cache:避免重复调用
面试诊断中有大量重复场景:同一道面试题的知识库检索、相似回答的诊断。Cache 层可以显著降低成本。
// cache.ts
import Database from 'better-sqlite3';
export interface CacheEntry {
key: string;
response: string; // JSON serialized ParsedResponse
tokensSaved: number;
createdAt: string;
expiresAt: string;
}
export class QueryCache {
private db: Database.Database;
constructor(dbPath: string) {
this.db = new Database(dbPath);
this.db.exec(`
CREATE TABLE IF NOT EXISTS query_cache (
key TEXT PRIMARY KEY,
response TEXT NOT NULL,
tokens_saved INTEGER NOT NULL,
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL
)
`);
}
get(key: string): ParsedResponse | null {
const row = this.db.prepare(
'SELECT response FROM query_cache WHERE key = ? AND expires_at > datetime("now")'
).get(key) as { response: string } | undefined;
return row ? JSON.parse(row.response) : null;
}
set(key: string, response: ParsedResponse, ttlSeconds: number): void {
this.db.prepare(`
INSERT OR REPLACE INTO query_cache (key, response, tokens_saved, created_at, expires_at)
VALUES (?, ?, ?, datetime('now'), datetime('now', '+' || ? || ' seconds'))
`).run(key, JSON.stringify(response), response.usage.inputTokens, ttlSeconds);
}
generateKey(params: StreamParams): string {
// 基于 model + messages + tools 生成确定性 hash
const payload = JSON.stringify({
model: params.model,
messages: params.messages,
tools: params.tools?.map(t => t.name),
temperature: params.temperature,
});
return createHash('sha256').update(payload).digest('hex').slice(0, 16);
}
evictExpired(): number {
const result = this.db.prepare(
'DELETE FROM query_cache WHERE expires_at <= datetime("now")'
).run();
return result.changes;
}
}
Router:按任务选模型
不是所有任务都需要最强模型。Router 根据任务类型自动选择。
// router.ts
export interface RouteRule {
task: string; // 任务标识
provider: string; // provider name
model: string; // 模型 id
reason: string; // 路由原因(debug 用)
}
const DEFAULT_ROUTES: RouteRule[] = [
{ task: 'diagnose_content', provider: 'claude', model: 'claude-sonnet-4-20250514', reason: '需要深度分析能力' },
{ task: 'diagnose_expression', provider: 'claude', model: 'claude-sonnet-4-20250514', reason: '需要语言理解能力' },
{ task: 'generate_report', provider: 'claude', model: 'claude-sonnet-4-20250514', reason: '长文本生成' },
{ task: 'knowledge_summary', provider: 'deepseek', model: 'deepseek-chat', reason: '轻量摘要,成本低' },
{ task: 'split_qa', provider: 'deepseek', model: 'deepseek-chat', reason: '结构化拆分,简单任务' },
{ task: 'cross_validate', provider: 'openai', model: 'gpt-4o', reason: '多模型交叉验证,避免单一偏差' },
{ task: 'embedding', provider: 'openai', model: 'text-embedding-3-small', reason: '向量化' },
];
export class ModelRouter {
private rules: RouteRule[];
constructor(rules?: RouteRule[]) {
this.rules = rules ?? DEFAULT_ROUTES;
}
resolve(task: string): { provider: string; model: string } {
const rule = this.rules.find(r => r.task === task);
if (!rule) {
// 默认走 Claude
return { provider: 'claude', model: 'claude-sonnet-4-20250514' };
}
return { provider: rule.provider, model: rule.model };
}
}
Token Counter:预算管理
每次诊断有成本上限,Token Counter 负责实时追踪。
// token-counter.ts
export interface TokenBudget {
maxInputTokens: number;
maxOutputTokens: number;
maxTotalCost: number; // 单位:美元
spent: {
inputTokens: number;
outputTokens: number;
totalCost: number;
requests: number;
};
}
export class TokenCounter {
private budget: TokenBudget;
constructor(budget: Partial<TokenBudget> = {}) {
this.budget = {
maxInputTokens: budget.maxInputTokens ?? 500_000,
maxOutputTokens: budget.maxOutputTokens ?? 100_000,
maxTotalCost: budget.maxTotalCost ?? 1.0,
spent: { inputTokens: 0, outputTokens: 0, totalCost: 0, requests: 0 },
};
}
record(usage: TokenUsage, provider: string): void {
this.budget.spent.inputTokens += usage.inputTokens;
this.budget.spent.outputTokens += usage.outputTokens;
this.budget.spent.totalCost += this.calculateCost(usage, provider);
this.budget.spent.requests += 1;
}
checkBudget(): { ok: boolean; reason?: string } {
if (this.budget.spent.totalCost >= this.budget.maxTotalCost) {
return { ok: false, reason: `Cost limit reached: $${this.budget.spent.totalCost.toFixed(4)}` };
}
if (this.budget.spent.inputTokens >= this.budget.maxInputTokens) {
return { ok: false, reason: 'Input token limit reached' };
}
return { ok: true };
}
getSummary(): string {
const { spent } = this.budget;
return `Requests: ${spent.requests} | Tokens: ${spent.inputTokens}in + ${spent.outputTokens}out | Cost: $${spent.totalCost.toFixed(4)}`;
}
private calculateCost(usage: TokenUsage, provider: string): number {
const pricing: Record<string, { input: number; output: number }> = {
claude: { input: 3.0 / 1_000_000, output: 15.0 / 1_000_000 },
openai: { input: 2.5 / 1_000_000, output: 10.0 / 1_000_000 },
deepseek: { input: 0.27 / 1_000_000, output: 1.10 / 1_000_000 },
};
const p = pricing[provider] ?? pricing.claude;
return usage.inputTokens * p.input + usage.outputTokens * p.output;
}
}
QueryEngine 主类:把所有模块组装起来
// engine.ts
export class QueryEngine {
private providers: Map<string, LLMProvider>;
private cache: QueryCache;
private rateLimiter: TokenBucketLimiter;
private router: ModelRouter;
private tokenCounter: TokenCounter;
constructor(config: QueryEngineConfig) {
this.providers = new Map();
this.providers.set('claude', new ClaudeProvider(config.anthropicApiKey));
this.providers.set('openai', new OpenAIProvider(config.openaiApiKey));
this.providers.set('deepseek', new DeepSeekProvider(config.deepseekApiKey));
this.cache = new QueryCache(config.cachePath);
this.rateLimiter = new TokenBucketLimiter(config.rateLimit ?? 10, config.refillRate ?? 2);
this.router = new ModelRouter(config.routes);
this.tokenCounter = new TokenCounter(config.budget);
}
async query(params: QueryParams): Promise<ParsedResponse> {
// 1. 预算检查
const budgetCheck = this.tokenCounter.checkBudget();
if (!budgetCheck.ok) {
throw new QueryEngineError(budgetCheck.reason!, 'rate_limit', false);
}
// 2. 路由选择
const route = params.task
? this.router.resolve(params.task)
: { provider: 'claude', model: params.model ?? 'claude-sonnet-4-20250514' };
// 3. 缓存检查
const streamParams: StreamParams = {
model: route.model,
messages: params.messages,
tools: params.tools,
maxTokens: params.maxTokens,
temperature: params.temperature,
systemPrompt: params.systemPrompt,
};
if (params.useCache !== false) {
const cacheKey = this.cache.generateKey(streamParams);
const cached = this.cache.get(cacheKey);
if (cached) return cached;
}
// 4. 限流等待
await this.rateLimiter.acquire();
// 5. 带重试的模型调用
const provider = this.providers.get(route.provider)!;
const response = await withRetry(async () => {
const events = provider.stream(streamParams);
return parseStream(events, params.onTextDelta);
});
// 6. 记录 token 消耗
this.tokenCounter.record(response.usage, route.provider);
// 7. 写入缓存
if (params.useCache !== false && response.type === 'text') {
const cacheKey = this.cache.generateKey(streamParams);
this.cache.set(cacheKey, response, params.cacheTtl ?? 3600);
}
return response;
}
getUsageSummary(): string {
return this.tokenCounter.getSummary();
}
}
上层使用示例:
const engine = new QueryEngine({
anthropicApiKey: process.env.ANTHROPIC_API_KEY!,
openaiApiKey: process.env.OPENAI_API_KEY!,
deepseekApiKey: process.env.DEEPSEEK_API_KEY!,
cachePath: './data/cache.db',
});
// 诊断任务——自动路由到 Claude
const diagnosis = await engine.query({
task: 'diagnose_content',
messages: [{ role: 'user', content: '请诊断这个回答...' }],
tools: diagnosticTools,
onTextDelta: (text) => process.stdout.write(text),
});
// 轻量任务——自动路由到 DeepSeek
const split = await engine.query({
task: 'split_qa',
messages: [{ role: 'user', content: '请拆分以下面试稿...' }],
});
// 交叉验证——路由到 OpenAI
const validation = await engine.query({
task: 'cross_validate',
messages: [{ role: 'user', content: '请验证以下诊断结论...' }],
});
错误处理流程
flowchart TD
ERR[模型调用异常] --> CLASSIFY{错误分类}
CLASSIFY -->|rate_limit| WAIT[等待 retry-after]
CLASSIFY -->|overloaded| BACKOFF[指数退避]
CLASSIFY -->|timeout| RETRY[立即重试]
CLASSIFY -->|network| RETRY
CLASSIFY -->|context_length| COMPACT[触发上下文压缩]
CLASSIFY -->|invalid_request| FAIL[直接失败 + 日志]
CLASSIFY -->|auth| FAIL
WAIT --> RETRY_CHECK{重试次数 < 3?}
BACKOFF --> RETRY_CHECK
RETRY --> RETRY_CHECK
RETRY_CHECK -->|是| CALL[重新调用]
RETRY_CHECK -->|否| FAIL
COMPACT --> CALL
CALL --> SUCCESS[返回结果]
context_length 错误比较特殊——不是简单重试,而是触发 Context Manager 的压缩逻辑,压缩后重新调用。这个跨模块协作是 Harness 架构的典型场景。
测试要点
单元测试:
- Provider: mock HTTP 响应,验证 StreamEvent 转换正确
- Retry: 模拟各种错误,验证重试策略
- Cache: 验证命中/未命中/过期逻辑
- Rate Limiter: 验证并发控制和排队
- Token Counter: 验证预算计算和阈值判断
集成测试:
- 真实调用三家 API(需要 API key)
- 验证流式输出完整性
- 验证 tool_use 解析正确
- 压力测试:并发 10 请求,验证限流生效
小结
- Query Engine 是 Harness 的可靠性边界,把模型 API 的不确定性收敛到一层
- 三个 Provider 统一了 Claude / OpenAI / DeepSeek 的流式接口差异
- Router 按任务自动选模型:重活给 Claude,轻活给 DeepSeek,验证给 OpenAI
- Retry 基于错误分类决定策略,不是无脑重试
- Cache + Rate Limiter + Token Counter 三重成本控制
- 所有代码手写,不依赖 LangChain,每一行都可讲解
下一篇建议继续看: