AI Agent
概述
BiliBili ShadowReplay 集成了基于 LangChain 的 AI Agent,用于内容分析、总结和智能辅助。Agent 实现位于 src/lib/agent/ 目录。
技术栈
- LangChain Core (@langchain/core): 核心框架
- LangChain DeepSeek (@langchain/deepseek): DeepSeek LLM 集成
- LangChain Ollama (@langchain/ollama): Ollama 本地模型支持
架构
主要功能
1. 内容分析
分析直播录播内容,提取关键信息:
typescript
import { analyzeContent } from '$lib/agent';
const analysis = await analyzeContent({
recordingId: 'xxx',
transcript: '直播文字记录...',
});
// 返回结果
{
summary: '直播内容总结',
highlights: ['精彩片段1', '精彩片段2'],
tags: ['游戏', '娱乐'],
suggestedTitle: '建议的标题'
}2. 智能切片建议
基于内容分析,建议切片时间点:
typescript
import { suggestClips } from '$lib/agent';
const suggestions = await suggestClips({
recordingId: 'xxx',
transcript: '...',
danmaku: [...], // 弹幕数据
});
// 返回建议的切片区间
[
{ start: 120, end: 300, reason: '精彩操作' },
{ start: 1200, end: 1500, reason: '搞笑片段' }
]3. 标题和描述生成
为切片生成标题和描述:
typescript
import { generateMetadata } from '$lib/agent';
const metadata = await generateMetadata({
clipContent: '切片内容描述',
context: '直播背景信息',
});
// 返回
{
title: '生成的标题',
description: '生成的描述',
tags: ['标签1', '标签2']
}LLM 配置
DeepSeek 配置
typescript
import { ChatDeepSeek } from '@langchain/deepseek';
const model = new ChatDeepSeek({
apiKey: 'your-api-key',
model: 'deepseek-chat',
temperature: 0.7,
});Ollama 本地模型
typescript
import { Ollama } from '@langchain/ollama';
const model = new Ollama({
baseUrl: 'http://localhost:11434',
model: 'llama2',
});Agent 实现
基础 Agent 结构
typescript
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { RunnableSequence } from '@langchain/core/runnables';
// 创建提示模板
const prompt = ChatPromptTemplate.fromMessages([
['system', '你是一个直播内容分析助手...'],
['human', '{input}'],
]);
// 创建处理链
const chain = RunnableSequence.from([
prompt,
model,
outputParser,
]);
// 执行
const result = await chain.invoke({
input: '分析这段直播内容...'
});带工具的 Agent
typescript
import { DynamicStructuredTool } from '@langchain/core/tools';
import { AgentExecutor, createOpenAIFunctionsAgent } from 'langchain/agents';
// 定义工具
const tools = [
new DynamicStructuredTool({
name: 'get_recording_info',
description: '获取录播信息',
schema: z.object({
recordingId: z.string(),
}),
func: async ({ recordingId }) => {
const info = await invoke('get_recording', { recordingId });
return JSON.stringify(info);
},
}),
new DynamicStructuredTool({
name: 'get_danmaku',
description: '获取弹幕数据',
schema: z.object({
recordingId: z.string(),
startTime: z.number().optional(),
endTime: z.number().optional(),
}),
func: async ({ recordingId, startTime, endTime }) => {
const danmaku = await invoke('get_danmaku', {
recordingId,
startTime,
endTime,
});
return JSON.stringify(danmaku);
},
}),
];
// 创建 Agent
const agent = await createOpenAIFunctionsAgent({
llm: model,
tools,
prompt,
});
// 创建执行器
const executor = new AgentExecutor({
agent,
tools,
});
// 执行任务
const result = await executor.invoke({
input: '分析录播 xxx 的内容并建议切片'
});提示工程
内容分析提示
typescript
const ANALYSIS_PROMPT = `你是一个专业的直播内容分析助手。
任务:分析给定的直播录播内容,提取关键信息。
输入信息:
- 直播标题:{title}
- 直播时长:{duration}
- 文字记录:{transcript}
- 弹幕数据:{danmaku}
请提供:
1. 内容总结(100字以内)
2. 3-5个精彩片段的时间点和描述
3. 5个相关标签
4. 建议的切片标题
输出格式:JSON
`;切片建议提示
typescript
const CLIP_SUGGESTION_PROMPT = `基于直播内容分析,建议值得制作成切片的片段。
考虑因素:
1. 弹幕密度突然增加的时间段
2. 文字记录中的关键词(如"精彩"、"哈哈"、"牛"等)
3. 情绪高涨的片段
4. 完整的故事或事件
每个建议包括:
- 开始时间
- 结束时间
- 片段描述
- 推荐理由
- 预估热度(1-10分)
输出格式:JSON数组
`;流式输出
对于长文本生成,使用流式输出提升用户体验:
typescript
import { writable } from 'svelte/store';
export const streamingText = writable('');
async function generateWithStreaming(input: string) {
streamingText.set('');
const stream = await chain.stream({ input });
for await (const chunk of stream) {
streamingText.update(text => text + chunk.content);
}
}在组件中使用:
svelte
<script>
import { streamingText } from '$lib/agent';
</script>
<div class="streaming-output">
{$streamingText}
</div>错误处理
typescript
async function safeAgentCall<T>(
agentFunc: () => Promise<T>,
fallback: T
): Promise<T> {
try {
return await agentFunc();
} catch (error) {
console.error('Agent call failed:', error);
// 检查是否是 API 限流
if (error.message.includes('rate limit')) {
throw new Error('API 调用频率过高,请稍后再试');
}
// 检查是否是网络错误
if (error.message.includes('network')) {
throw new Error('网络连接失败,请检查网络设置');
}
// 返回默认值
return fallback;
}
}性能优化
1. 缓存结果
typescript
const analysisCache = new Map<string, any>();
async function analyzeWithCache(recordingId: string) {
if (analysisCache.has(recordingId)) {
return analysisCache.get(recordingId);
}
const result = await analyzeContent({ recordingId });
analysisCache.set(recordingId, result);
return result;
}2. 批量处理
typescript
async function batchAnalyze(recordingIds: string[]) {
// 并发处理,但限制并发数
const concurrency = 3;
const results = [];
for (let i = 0; i < recordingIds.length; i += concurrency) {
const batch = recordingIds.slice(i, i + concurrency);
const batchResults = await Promise.all(
batch.map(id => analyzeContent({ recordingId: id }))
);
results.push(...batchResults);
}
return results;
}3. 超时控制
typescript
async function analyzeWithTimeout(input: any, timeout = 30000) {
return Promise.race([
analyzeContent(input),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Analysis timeout')), timeout)
),
]);
}配置管理
在用户配置中管理 LLM 设置:
typescript
interface LLMConfig {
provider: 'deepseek' | 'ollama';
apiKey?: string;
baseUrl?: string;
model: string;
temperature: number;
maxTokens: number;
}
// 从配置加载
export async function loadLLMConfig(): Promise<LLMConfig> {
return await invoke('get_llm_config');
}
// 保存配置
export async function saveLLMConfig(config: LLMConfig) {
await invoke('save_llm_config', { config });
}最佳实践
- 提示优化: 清晰、具体的提示能获得更好的结果
- 错误处理: 始终处理 API 调用可能的失败
- 用户反馈: 显示加载状态和进度
- 成本控制: 缓存结果,避免重复调用
- 隐私保护: 不要将敏感信息发送到外部 API
调试
typescript
// 启用调试日志
if (import.meta.env.DEV) {
// 记录所有 LLM 调用
const originalInvoke = chain.invoke;
chain.invoke = async (input) => {
console.log('[LLM Input]', input);
const result = await originalInvoke.call(chain, input);
console.log('[LLM Output]', result);
return result;
};
}示例:完整的分析流程
typescript
import { ChatDeepSeek } from '@langchain/deepseek';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { invoke } from '@tauri-apps/api/core';
export async function analyzeRecording(recordingId: string) {
// 1. 获取录播信息
const recording = await invoke('get_recording', { recordingId });
// 2. 获取字幕(如果有)
const subtitle = await invoke('get_subtitle', { recordingId });
// 3. 获取弹幕数据
const danmaku = await invoke('get_danmaku', { recordingId });
// 4. 准备输入
const input = {
title: recording.title,
duration: recording.duration,
transcript: subtitle?.text || '',
danmakuCount: danmaku.length,
danmakuSample: danmaku.slice(0, 100), // 采样
};
// 5. 调用 LLM
const model = new ChatDeepSeek({
apiKey: await invoke('get_deepseek_key'),
model: 'deepseek-chat',
});
const prompt = ChatPromptTemplate.fromTemplate(ANALYSIS_PROMPT);
const chain = prompt.pipe(model);
const result = await chain.invoke(input);
// 6. 解析结果
return JSON.parse(result.content);
}