17.11-批量AI生成任务
要点
- 批量 AI 生成:一次调用 LLM 生成几十、几百条内容——批量摘要、批量翻译、批量分类
- 并发控制很重要——同时调 100 次 LLM API 会触发限流
- 成本控制——批量生成烧钱,需要追踪 token 用量和费用
- 分批处理——把大批量拆成小批,每批处理完更新进度
- 失败处理——部分失败时保留已成功的结果,不全部重来
1. 批量生成的场景
- 批量摘要:100 篇文档,每篇生成 200 字摘要
- 批量翻译:把网站 500 个页面从中文翻译成英文
- 批量分类:给 1000 篇文章打标签
- 批量提取:从 200 份合同里提取关键条款
- 批量改写:把 50 条产品描述改写成营销文案
这些场景的共同特点:
- 输入是批量数据(几十到几千条)
- 每条独立调用 LLM(或批量调用)
- 总耗时长(几分钟到几小时)
- 需要异步处理
2. 任务定义
interface BatchGenerateTask {
batchId: string
userId: string
items: Array<{
id: string
input: string
metadata?: Record<string, unknown>
}>
prompt: string // 系统 prompt 或模板
model: string // 模型名称
options: {
maxTokens: number
temperature: number
concurrency: number // 并发数
}
}
interface BatchResult {
batchId: string
status: 'pending' | 'processing' | 'completed' | 'failed'
totalItems: number
completedItems: number
failedItems: number
totalTokens: number
totalCost: number
results: Array<{
itemId: string
output?: string
tokens: number
error?: string
}>
}3. 分批处理
大批量不能一次性处理——需要拆成小批,每批并发处理。
async function batchGenerate(
task: BatchGenerateTask,
env: Env
): Promise<BatchResult> {
const { items, prompt, model, options } = task
const { concurrency } = options
const result: BatchResult = {
batchId: task.batchId,
status: 'processing',
totalItems: items.length,
completedItems: 0,
failedItems: 0,
totalTokens: 0,
totalCost: 0,
results: [],
}
// 分批处理
for (let i = 0; i < items.length; i += concurrency) {
const batch = items.slice(i, i + concurrency)
// 并发处理当前批次
const promises = batch.map((item) =>
generateSingle(item, prompt, model, options, env)
.then((res) => ({ ...res, itemId: item.id }))
.catch((err) => ({
itemId: item.id,
error: err.message,
tokens: 0,
}))
)
const batchResults = await Promise.all(promises)
// 合并结果
for (const res of batchResults) {
if (res.error) {
result.failedItems++
} else {
result.completedItems++
result.totalTokens += res.tokens
result.totalCost += estimateCost(res.tokens, model)
}
result.results.push(res)
}
// 更新进度
await updateBatchProgress(env, task.batchId, result)
// 限流:批次之间等待
if (i + concurrency < items.length) {
await sleep(1000) // 1 秒间隔
}
}
result.status = result.failedItems === result.totalItems ? 'failed' : 'completed'
return result
}4. 单条生成
async function generateSingle(
item: { id: string; input: string },
prompt: string,
model: string,
options: { maxTokens: number; temperature: number },
env: Env
): Promise<{ output: string; tokens: number }> {
// 替换模板变量
const finalPrompt = prompt.replace('{{input}}', item.input)
// 调用 LLM API
const response = await env.AI.run(model, {
messages: [
{ role: 'system', content: finalPrompt },
{ role: 'user', content: item.input },
],
max_tokens: options.maxTokens,
temperature: options.temperature,
})
const output = response.choices?.[0]?.message?.content ?? ''
const tokens = response.usage?.total_tokens ?? estimateTokens(output)
return { output, tokens }
}5. 并发控制
并发数(concurrency)控制同时调用 LLM 的次数。太高会触发限流,太低处理太慢。
// 根据模型调整并发数
const MODEL_CONCURRENCY: Record<string, number> = {
'@cf/openai/gpt-4o-mini': 10, // 小模型,可以高并发
'@cf/openai/gpt-4o': 5, // 大模型,控制并发
'@cf/anthropic/claude-3-haiku': 10,
'@cf/anthropic/claude-3-sonnet': 5,
'@cf/anthropic/claude-3-opus': 2, // 最贵的模型,低并发
}
function getConcurrency(model: string): number {
return MODEL_CONCURRENCY[model] ?? 5
}动态调整并发
如果检测到限流,降低并发:
async function batchGenerateWithAdaptiveConcurrency(
task: BatchGenerateTask,
env: Env
): Promise<BatchResult> {
let concurrency = task.options.concurrency
const result = initResult(task)
for (let i = 0; i < task.items.length; i += concurrency) {
const batch = task.items.slice(i, i + concurrency)
try {
const batchResults = await Promise.all(
batch.map((item) => generateSingle(item, task.prompt, task.model, task.options, env))
)
// 成功,合并结果
mergeResults(result, batchResults)
// 连续成功 3 次,尝试提高并发
if (consecutiveSuccesses >= 3 && concurrency < 20) {
concurrency++
}
} catch (err) {
if (err.message.includes('rate limit')) {
// 限流,降低并发
concurrency = Math.max(1, Math.floor(concurrency / 2))
await sleep(5000) // 等 5 秒再试
} else {
// 其他错误
mergeError(result, err)
}
}
await updateBatchProgress(env, task.batchId, result)
await sleep(1000)
}
return result
}6. 成本控制
批量生成烧钱。需要追踪 token 用量和费用。
// 估算 token 数(粗略)
function estimateTokens(text: string): number {
const cjk = (text.match(/[一-鿿]/g) || []).length
return Math.ceil(cjk / 1.5 + (text.length - cjk) / 4)
}
// 估算费用
function estimateCost(tokens: number, model: string): number {
const PRICING: Record<string, number> = {
'@cf/openai/gpt-4o-mini': 0.00015, // $0.15 / 1M tokens
'@cf/openai/gpt-4o': 0.0025, // $2.50 / 1M tokens
'@cf/anthropic/claude-3-haiku': 0.00025,
'@cf/anthropic/claude-3-sonnet': 0.003,
'@cf/anthropic/claude-3-opus': 0.015,
}
const pricePerToken = PRICING[model] ?? 0.001
return tokens * pricePerToken
}
// 检查预算
async function checkBudget(userId: string, estimatedCost: number, env: Env): Promise<boolean> {
const user = await env.DB.prepare(`
SELECT budget_limit, budget_used FROM users WHERE id = ?
`).bind(userId).first()
if (!user) return false
if (user.budget_limit === null) return true // 无限制
return (user.budget_used + estimatedCost) <= user.budget_limit
}在批量任务开始前检查预算:
app.post('/api/batch-generate', async (c) => {
const { items, prompt, model } = await c.req.json()
// 估算总 token 和费用
const estimatedTokens = items.reduce((sum, item) => {
return sum + estimateTokens(item.input) + 200 // 200 是输出预估
}, 0)
const estimatedCost = estimateCost(estimatedTokens, model)
// 检查预算
if (!await checkBudget(c.get('user').id, estimatedCost, c.env)) {
return c.json({ error: '预算不足' }, 402)
}
// 创建任务...
})7. 进度追踪
批量任务的进度:已完成 X / Y 条,失败 Z 条。
CREATE TABLE batch_tasks (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
total_items INTEGER NOT NULL,
completed_items INTEGER NOT NULL DEFAULT 0,
failed_items INTEGER NOT NULL DEFAULT 0,
total_tokens INTEGER NOT NULL DEFAULT 0,
total_cost REAL NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE batch_results (
id TEXT PRIMARY KEY,
batch_id TEXT NOT NULL,
item_id TEXT NOT NULL,
output TEXT,
tokens INTEGER NOT NULL DEFAULT 0,
error TEXT,
created_at TEXT NOT NULL
);async function updateBatchProgress(env: Env, batchId: string, result: BatchResult): Promise<void> {
await env.DB.prepare(`
UPDATE batch_tasks
SET completed_items = ?, failed_items = ?, total_tokens = ?,
total_cost = ?, status = ?, updated_at = ?
WHERE id = ?
`).bind(
result.completedItems,
result.failedItems,
result.totalTokens,
result.totalCost,
result.status,
new Date().toISOString(),
batchId,
).run()
}前端轮询:
async function pollBatchProgress(batchId: string): Promise<void> {
const interval = setInterval(async () => {
const data = await fetch(`/api/batch-tasks/${batchId}`).then(r => r.json())
updateProgressBar(data.completedItems / data.totalItems * 100)
updateStats({
completed: data.completedItems,
failed: data.failedItems,
total: data.totalItems,
cost: data.total_cost,
})
if (data.status === 'completed' || data.status === 'failed') {
clearInterval(interval)
}
}, 3000)
}8. 结果查询
批量完成后,查询每条结果:
app.get('/api/batch-tasks/:batchId/results', async (c) => {
const batchId = c.req.param('batchId')
const results = await c.env.DB.prepare(`
SELECT item_id, output, tokens, error
FROM batch_results
WHERE batch_id = ?
ORDER BY created_at
`).bind(batchId).all()
return c.json({
batchId,
results: results.results,
})
})
// 导出为 CSV
app.get('/api/batch-tasks/:batchId/export', async (c) => {
const results = await c.env.DB.prepare(`
SELECT item_id, output FROM batch_results WHERE batch_id = ? AND output IS NOT NULL
`).bind(c.req.param('batchId')).all()
const csv = [
'item_id,output',
...results.results.map((r) => `${r.item_id},"${r.output.replace(/"/g, '""')}"`),
].join('\n')
return new Response(csv, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': `attachment; filename="batch-${c.req.param('batchId')}.csv"`,
},
})
})9. 失败处理
部分失败时保留已成功的结果:
async function batchGenerateWithRetry(
task: BatchGenerateTask,
env: Env
): Promise<BatchResult> {
const result = initResult(task)
const failedItems: typeof task.items = []
// 第一轮处理
for (let i = 0; i < task.items.length; i += task.options.concurrency) {
const batch = task.items.slice(i, i + task.options.concurrency)
const promises = batch.map(async (item) => {
try {
const res = await generateSingle(item, task.prompt, task.model, task.options, env)
return { ...res, itemId: item.id, success: true }
} catch (err) {
return { itemId: item.id, error: err.message, success: false }
}
})
const batchResults = await Promise.all(promises)
for (const res of batchResults) {
if (res.success) {
result.completedItems++
result.totalTokens += res.tokens
saveResult(env, task.batchId, res)
} else {
result.failedItems++
const failedItem = task.items.find((i) => i.id === res.itemId)!
failedItems.push(failedItem)
}
}
await updateBatchProgress(env, task.batchId, result)
}
// 第二轮重试失败项
if (failedItems.length > 0) {
await sleep(5000) // 等 5 秒
for (const item of failedItems) {
try {
const res = await generateSingle(item, task.prompt, task.model, task.options, env)
result.completedItems++
result.failedItems--
result.totalTokens += res.tokens
saveResult(env, task.batchId, { ...res, itemId: item.id })
} catch (err) {
// 重试还是失败,记录错误
saveError(env, task.batchId, item.id, err.message)
}
await updateBatchProgress(env, task.batchId, result)
}
}
return result
}10. 完整流程
// 1. 提交批量任务
app.post('/api/batch-generate', async (c) => {
const { items, prompt, model } = await c.req.json()
const batchId = crypto.randomUUID()
const now = new Date().toISOString()
await c.env.DB.prepare(`
INSERT INTO batch_tasks (id, user_id, total_items, status, created_at, updated_at)
VALUES (?, ?, ?, 'pending', ?, ?)
`).bind(batchId, c.get('user').id, items.length, now, now).run()
await c.env.BATCH_QUEUE.send({
batchId,
userId: c.get('user').id,
items,
prompt,
model,
options: {
maxTokens: 500,
temperature: 0.7,
concurrency: getConcurrency(model),
},
})
return c.json({ batchId, status: 'pending' }, 202)
})
// 2. Queue 消费者
export async function batchConsumer(
batch: MessageBatch<BatchGenerateTask>,
env: Env
): Promise<void> {
for (const msg of batch.messages) {
const result = await batchGenerate(msg.body, env)
await saveBatchResult(env, result)
msg.ack()
}
}总结
回顾这一节的要点:
- 批量 AI 生成:一次调 LLM 生成几十到几千条内容
- 并发控制很重要——太高触发限流,太低处理太慢
- 分批处理:拆成小批,每批并发,批次间等待
- 动态调整并发:检测到限流时降低并发,连续成功时提高
- 成本控制:估算 token 和费用,检查预算
- 进度追踪:已完成 X / Y 条,失败 Z 条,总费用
- 失败处理:部分失败保留已成功的,失败项可重试
- 结果导出:支持 JSON、CSV 格式
下一篇讲工作流编排——多个任务如何串联成复杂流程。