【全栈实践】第一个 AI Agent 项目:从零搭建 AI 音频创作助手(高级篇)
ãå ¨æ å®è·µã第ä¸ä¸ª AI Agent 项ç®ï¼ä»é¶æå»º AI é³é¢åä½å©æï¼é«çº§ç¯ï¼
京ä¸äºå¼åè 2026-08-17 0 é 读8åé项ç®ä»ç»
è¿æ¯ä¸ä¸ª AI 驱å¨çæå®¢åä½å©æï¼ä¸æ³¨äºä»æå/è§é¢/é³é¢å 容è¯å«å°æå®¢å¶ä½çå ¨æµç¨ãä½ å¯ä»¥ç¨èªç¶è¯è¨ä¸å®å¯¹è¯ï¼ä¹å¯ä»¥ä¸ä¼ è§é¢åé³é¢ï¼å®ä¼çè§£æå¾ãè°ç¨ç¸åºçè¯é³åæå·¥å ·ï¼æä»ä»¬åæçå®çæå®¢é³é¢ã
å ³äºé¡¹ç®ç详ç»ä»ç»è§ä»¥ä¸æç« ï¼
1.第ä¸ä¸ª AI Agent 项ç®ï¼ä»é¶æå»º AI é³é¢åä½å©æï¼å ¥é¨ç¯ï¼ï¼ è¿ä¸ªæ¯ç¬¬ä¸çæ¬ï¼éé¢ä¸»è¦è¿è¡åææ¡æ¶æå»ºï¼å å«Vue3 + ai-elements-vue æå端çé¢ï¼LangChain 1.0 à AG-UI åè®® à Qwen TTS 驱å¨åç«¯ï¼æ¯æå®å¶é³è²ï¼å¤å»é³è²åé é³åè½ã
2.第ä¸ä¸ª AI Agent 项ç®ï¼ä»é¶æå»º AI é³é¢åä½å©æï¼è¿é¶ç¯ï¼ ï¼ è¿ä¸ªæ¯ç¬¬äºçæ¬ï¼æ°å¢äºå®æ´çæå®¢åæå¶ä½è½åï¼å æ¬é³é¢æ¼æ¥ãæºè½BGMéæ©ãé³è½¨æ··é³ä¸å¤§æ ¸å¿åè½ï¼åæ¶æ°å¢äºé³é¢èµæºç®¡çAPIåè¯é³è¾å ¥åè½ï¼å®ç°äºä»å端ä¸ä¼ ãè¯é³è¾å ¥å°å端å¤ççå ¨é¾è·¯éç¯ã
è¿ç¯æç« æ´å¤ä»ç»é«é¶ç¯æ·»å çåè½ã
æ´æ°æ¦è§
æ¬æ¬¡æ´æ°æ°å¢äºé³é¢ãè§é¢è¯å«è½åã临æ¶-æ°¸ä¹ åå±å卿¶æã宿¶æ¸ çæºå¶åæ´æºè½çæç¤ºè¯ç³»ç»ï¼è®©æå®¢ Agent ä»å纯ç"è¯é³åæå·¥å ·"å级为"å ¨é¾è·¯é³é¢å 容åä½å¹³å°"ã
ä¸ãåç«¯æ´æ°
1. é³é¢/è§é¢æä»¶åå¨è·¯å¾éæ
åæ´æä»¶ï¼backend/app/tools/audio_index.py
åå
é³é¢æä»¶ç»ä¸ä¿åå¨Â storage/audios/ ç®å½ä¸ï¼ç°å¨æ¹ä¸ºä¿åå°ä¸´æ¶ç®å½Â storage/temp/ï¼å®ç°ä¸´æ¶æä»¶ä¸æ°¸ä¹
æä»¶çå离管çã
æ ¸å¿æ¹å¨ï¼æ°å¢Â save_media_to_temp()Â å½æ°ï¼æ¯æä¸ç§è¾å
¥ç±»åï¼
def save_media_to_temp(media_data, filename: str) -> str:
"""å°é³é¢/è§é¢æä»¶ä¿åå°ä¸´æ¶ç®å½"""
temp_dir = STORAGE_DIR / "temp"
temp_dir.mkdir(parents=True, exist_ok=True)
file_path = temp_dir / filename
if isinstance(media_data, str):
# base64 å符串ï¼è§£ç ä¿å
file_data = base64.b64decode(media_data)
file_path.write_bytes(file_data)
elif isinstance(media_data, bytes):
# bytes 对象ï¼ç´æ¥ä¿å
file_path.write_bytes(media_data)
else:
# AudioSegment 对象ï¼å¯¼åºæä»¶
file_format = filename.split('.')[-1]
media_data.export(str(file_path), format=file_format)
return f"storage/temp/{filename}"
设计æè·¯ï¼é³è²è®¾è®¡ã声é³å
éçå·¥å
·çæçä¸é´äº§ç©ç»ä¸æ¾å¨Â storage/temp/ï¼å½ç¨æ·ç¡®è®¤æ»¡æååéè¿Â save_voice 工å
·å°é³è²æ°¸ä¹
è¿ç§»å°Â storage/audios/ãè¿æ ·æ¢é¿å
äºæ°¸ä¹
ç®å½çè¨èï¼ä¹è®©æä»¶çå½å¨æç®¡çæ´æ¸
æ°ã
2. 宿¶ä¸´æ¶æä»¶æ¸ çä»»å¡
åæ´æä»¶ï¼backend/app/utils/temp_cleanup.pyï¼æ°å¢ï¼
æ°å¢å®æ¶æ¸
çæºå¶ï¼èªå¨æ¸
é¤Â storage/temp/ ç®å½ä¸è¶
è¿æå®æ¶é´çè¿ææä»¶ï¼é²æ¢ä¸´æ¶æä»¶å 积å ç¨ç£ç空é´ã
æ ¸å¿å®ç°ï¼
def cleanup_temp_files(max_age_minutes: int = 10) -> dict:
"""æ¸
çè¿æçä¸´æ¶æä»¶"""
cutoff_time = time.time() - (max_age_minutes * 60)
for file_path in TEMP_DIR.rglob("*"):
if not file_path.is_file():
continue
file_mtime = file_path.stat().st_mtime
if file_mtime < cutoff_time:
file_size = file_path.stat().st_size
file_path.unlink()
stats["deleted_files"] += 1
stats["freed_bytes"] += file_size
# æ¸
ç空ç®å½
cleanup_empty_dirs(TEMP_DIR)
return stats
å ³é®ç¹æ§ï¼
- é»è®¤ä¿çæ¶é´ 10 åéï¼å¯çµæ´»é ç½®
- é彿¸ çææåç®å½ï¼å¹¶èªå¨ç§»é¤ç©ºç®å½
- è¿å详ç»ç»è®¡ä¿¡æ¯ï¼æä»¶æ°ãå 餿°ãéæ¾ç©ºé´ï¼
- æä¾Â
schedule_cleanup_task()Â å½æ°ï¼æ¹ä¾¿æ¥å ¥ APScheduler ç宿¶è°åº¦æ¡æ¶
3. é³è²ä¿åå·¥å ·
åæ´æä»¶ï¼backend/app/tools/voice_save.pyï¼æ°å¢ï¼
æ°å¢Â save_voice 工å
·ï¼å°ç¨æ·æ»¡æçå®å¶é³è²ä»ä¸´æ¶ç®å½æ°¸ä¹
ä¿åå°Â storage/audios/ï¼å¹¶è®°å½å°Â voice_index.jsonÂ ç´¢å¼æä»¶ã
æ ¸å¿é»è¾ï¼
@tool("save_voice", args_schema=VoiceSaveInput)
def save_voice_tool(audio_source: str, voice_id: str, text: str, model_name: str = "") -> str:
# 夿è¾å
¥ç±»åï¼æä»¶è·¯å¾ æ base64æ°æ®
if audio_source.startswith("storage/") or audio_source.startswith("/"):
# ä»ä¸´æ¶ç®å½å¤å¶å°æ°¸ä¹
ç®å½
source_path = BASE_DIR / audio_source.lstrip("/")
dest_path = AUDIOS_DIR / source_path.name
shutil.copy2(source_path, dest_path)
local_path = f"storage/audios/{source_path.name}"
else:
# base64 æ°æ®ï¼è§£ç å¹¶ä¿å
local_path = save_audio_from_base64(audio_source, text, "saved_voice")
# è®°å½å°ç´¢å¼æä»¶
record_voice_index(local_path, voice_id, model_name, path="audios")
return json.dumps({"audio_url": local_path, "voice_id": voice_id, ...})
设计亮ç¹ï¼
- æ¯æä¸¤ç§è¾å ¥æ¹å¼ï¼ä¸´æ¶æä»¶è·¯å¾ï¼ç´æ¥å¤å¶ï¼å base64 ç¼ç æ°æ®ï¼è§£ç ä¿åï¼
- èªå¨çæå¯ä¸æä»¶åï¼å 嫿¶é´æ³ãUUIDãé³è²IDåææ¬ç段
- ä½¿ç¨æä»¶éï¼
fcntl.flockï¼ä¿è¯ç´¢å¼æä»¶å¹¶ååå ¥å®å ¨ - 䏿客工使µæ ç¼è¡æ¥ï¼é³è²è®¾è®¡ â 临æ¶ä¿å â ç¨æ·ç¡®è®¤ â æ°¸ä¹ ä¿å
4. è¯é³è¯å«å·¥å ·ï¼ASRï¼
åæ´æä»¶ï¼backend/app/tools/qwen_asr.pyï¼æ°å¢ï¼
éæé¿éäº DashScope qwen3-asr-flash 模åï¼æä¾é«ç²¾åº¦è¯é³è¯å«è½åã
æ ¸å¿è°ç¨é¾ï¼
@tool("qwen_asr_tool", args_schema=ASRInput)
def qwen_asr_tool(audio: str, enable_itn: bool = False) -> str:
# 1. è§£æé³é¢æ¥æºï¼URL / æ¬å°è·¯å¾ â data URIï¼
audio_url = _resolve_audio_source(audio)
# 2. è°ç¨ ASR API
response = _call_asr_api(audio_url, enable_itn)
# 3. æåè¯å«ææ¬
text = _extract_text_from_response(response)
return text
å ³é®è½åï¼
- 夿 ¼å¼æ¯æï¼MP3ãWAVãOGGãFLACãM4AãAAC ç主æµé³é¢æ ¼å¼
- ITN éææ¬æ ååï¼èªå¨å°å£è¯å表达转为书é¢å½¢å¼ï¼å¦"äºé¶äºäºå¹´" â "2025å¹´"ï¼
- æºè½æ¥æºè§£æï¼æ¯ææ¬å°è·¯å¾åç½ç» URLï¼æ¬å°æä»¶èªå¨è½¬ä¸º base64 data URI
- MIME ç±»åèªå¨æ¨æï¼æ ¹æ®æä»¶æ©å±åå¹é æ£ç¡®ç MIME ç±»å
5. 夿¨¡æè¯å«å·¥å ·
åæ´æä»¶ï¼backend/app/tools/qwen_multimodal.pyï¼æ°å¢ï¼
éæé¿éäº DashScope qwen3.5-omni-plus 模åï¼æ¯æå¾çãè§é¢ãé³é¢çç»ä¸å¤æ¨¡æçè§£ã
ä¸¤å¤§æ ¸å¿å·¥å ·ï¼
| å·¥å ·å | åè½ | éç¨åºæ¯ |
|---|---|---|
qwen_multimodal_tool | ååªä½å¤æ¨¡æè¯å« | åæåå¼ å¾ç/å个è§é¢/åæ®µé³é¢ |
qwen_combined_multimodal_tool | ç»å夿¨¡æè¯å« | åæ¶åæå¤ä¸ªåªä½ï¼å¦å¾ç+é³é¢ç»åï¼ |
大è§é¢æºè½åå²ï¼å½è§é¢æä»¶è¶
è¿ 21MB æ¶ï¼èªå¨ä½¿ç¨Â moviepy åå²ä¸ºå¤ä¸ªç段åå«å¤çï¼
def _split_and_encode_video(video_path: Path, suffix: str) -> List[dict]:
video = VideoFileClip(str(video_path))
target_size = 10 * 1024 * 1024 # æ¯æ®µ 10MB
num_segments = max(1, int(file_size / target_size))
segment_duration = duration / num_segments
for i in range(num_segments):
segment = video.subclipped(start_time, end_time)
# ç¼ç å¹¶æ·»å å°ç段å表
segments.append(_build_media_content(data_uri, suffix, is_url=False))
return segments
é¢å¤è½åï¼
- æ¯ææµå¼è¾åºï¼
stream=Trueï¼ï¼å®æ¶è·åè¯å«ç»æ - æ¯æé³é¢è¾åºæ¨¡æï¼
enable_audio_outputï¼ï¼å¯æå®é³è²è¿è¡è¯é³åæ - å®åçé误å¤ç忥å¿è¿½è¸ª
6. æç¤ºè¯ä¼å
åæ´æä»¶ï¼backend/app/services/prompt.py
对æå®¢ Agent çç³»ç»æç¤ºè¯è¿è¡äºå ¨é¢ç²¾ç¼åå¢å¼ºï¼
ä¼åè¦ç¹ï¼
- ç»æéç»ï¼å°åæ¬æä¹±çæç¤ºè¯æåä¸ºæ¸ æ°ç模ååç»æââæ ¸å¿è½åå®ä½ã工使¹å¼ãå·¥å ·è°ç¨è§åãæ²éè§èãä¸ä¸æçè§£ãæ§è¡æµç¨
- æ°å¢é³è§é¢è¯å«å
容ï¼å¨å·¥ä½æµä¸æç¡®åºåè¯é³è¯å«ï¼
qwen_asr_toolï¼å夿¨¡æçè§£ï¼qwen_multimodal_tool /Âqwen_combined_multimodal_toolï¼ç使ç¨åºæ¯ - å·¥å ·ä½¿ç¨çç¥è¡¨æ ¼åï¼ç¨æ¸ æ°çæ å°å ³ç³»è¯´æåå·¥å ·èè´£ï¼éä½ LLM 误è°ç¨æ¦ç
- é³è²ä¿åæµç¨æ ååï¼æç¡®"设计 â 临æ¶ä¿å â ç¨æ·ç¡®è®¤ â æ°¸ä¹ ä¿å"ç忥æµç¨
- é¿å éå¤è°ç¨ï¼å¨æç¤ºè¯ä¸å¼ºè°"对äºç®å请æ±ï¼è°ç¨ä¸æ¬¡å·¥å ·åç«å³ç»æåå¤"
æ°å¢çé³è§é¢è¯å«å·¥ä½æµæè¿°ï¼
**é³è§é¢è½¬æå®¢å·¥ä½æµ**ï¼
1. å
容è¯å«ï¼è°ç¨ qwen_multimodal_tool æ qwen_asr_tool è¯å«é³è§é¢ä¸çå
容
2. èæ¬æ´çï¼åºäºè¯å«åºçå
å®¹ï¼æ´ç为æå®¢èæ¬
3. æå®¢å¶ä½ï¼æç
§å·¥ä½æµæ¥éª¤ç»§ç»æ§è¡ï¼å®ææå®¢å¶ä½
äºãåç«¯æ´æ°
æ¬æ¬¡æ´æ°å端æ åæ´ï¼ä¸»è¦éä¸å¨å端è½åçæ©å±åä¼åãåç«¯ç°æç ChatAgent.vue 已å
·å¤é³è§é¢æä»¶ä¸ä¼ ãææ¾é¢è§ãå·¥å
·è°ç¨ç»æå±ç¤ºçè½åï¼è½å¤å®æ´æ¿æ¥å端æ°å¢ç ASR å夿¨¡æè¯å«åè½ã
ä¸ãæ¶ææ»ç»
æ¬æ¬¡æ´æ°çæ ¸å¿èç»ï¼
ç¨æ·ä¸ä¼ é³è§é¢
â
â¼
âââââââââââââââââââââââââââââââââââââââââââââââ
â å
容è¯å«å± â
â ââ qwen_asr_tool è¯é³âæå â
â ââ qwen_multimodal_tool é³è§é¢âçè§£ â
âââââââââââââââââââââââââââââââââââââââââââââââ
â
â¼
âââââââââââââââââââââââââââââââââââââââââââââââ
â æå®¢å¶ä½å± â
â ââ é³è²è®¾è®¡ â 临æ¶ç®å½ (storage/temp/) â
â ââ save_voice â æ°¸ä¹
ç®å½ (storage/audios/) â
â ââ é³é¢åæ / æ··é³ / æ¼æ¥ â
âââââââââââââââââââââââââââââââââââââââââââââââ
â
â¼
âââââââââââââââââââââââââââââââââââââââââââââââ
â ç³»ç»ç»´æ¤å± â
â ââ temp_cleanup 宿¶æ¸
çä¸´æ¶æä»¶ â
âââââââââââââââââââââââââââââââââââââââââââââââ
éè¿"临æ¶-æ°¸ä¹ "åå±å卿¶æ + 宿¶æ¸ çæºå¶ï¼æ¢ä¿è¯äºæå®¢å¶ä½æµç¨ççµæ´»æ§ï¼ä¹ç¡®ä¿äºç³»ç»èµæºçææç®¡çã
åãææ¯äº®ç¹
1. 临æ¶-æ°¸ä¹ åå±å卿¶æ
æ¬æ¬¡æ´æ°ææ ¸å¿çæ¶æè®¾è®¡ãå·¥å
·çæçä¸é´äº§ç©ï¼é³è²è®¾è®¡ã声é³å
éç»æï¼ç»ä¸åå
¥Â storage/temp/ï¼åªæç¨æ·æç¡®ç¡®è®¤æ»¡æçé³è²æéè¿Â save_voice 工å
·è¿ç§»å°Â storage/audios/ãè¿ç§è®¾è®¡ï¼
- é¿å åå¨è¨èï¼ä¸´æ¶æä»¶æ 10 åéçå½å¨æï¼è¿æèªå¨æ¸ ç
- è¯ä¹æ¸
æ°ï¼
temp/ = æªç¡®è®¤çä¸é´äº§ç©ï¼audios/ = ç¨æ·ç¡®è®¤çæç»äº§ç© - ç¨æ·å¯æ§ï¼ç¨æ·æ¥æ"ä¿å"å"丢å¼"ç䏻卿
2. 大è§é¢æºè½å段çç¥
qwen_multimodal.py ä¸å½è§é¢æä»¶è¶
è¿ 21MB æ¶ï¼èªå¨ç¨Â moviepyÂ ææ¶é´è½´åååå为å¤ä¸ª â¤10MB ççæ®µåå«ä¸ä¼ è¯å«ãè¿è§£å³äºå¤§æ¨¡å API 对åæä»¶å¤§å°çéå¶é®é¢ï¼åæ¶ä¿è¯äºè¯å«å®æ´æ§ã
3. å¹¶åå®å ¨çç´¢å¼æä»¶åå ¥
voice_save.py 使ç¨Â fcntl.flock 对 voice_index.jsonÂ å æä»éï¼ç¡®ä¿å¤è¯·æ±å¹¶ååå
¥æ¶ä¸ä¼åºç°æ°æ®ç«äºæç´¢å¼æåã
4. 模ååæç¤ºè¯å·¥ç¨
prompt.py å°ç³»ç»æç¤ºè¯æå为 6 个ç¬ç«æ¨¡åï¼è½åå®ä½ã工使¹å¼ãå·¥å
·è§åãæ²éè§èãä¸ä¸æçè§£ãæ§è¡æµç¨ï¼ï¼æ¯ä¸ªæ¨¡åèè´£åä¸ãè¾¹çæ¸
æ°ï¼å¤§å¹
éä½äº LLM 误è°ç¨å·¥å
·çæ¦çï¼ä¹ä¾¿äºåç»è¿ä»£ç»´æ¤ã
äº.ç»æå±ç¤º
Aitishiku.com