- Published on
第一次學 Kotlin Koog AI 就上手 Day 24:子圖與模組化:建構複雜 AI 應用
在前面兩篇文章中,我們學習了策略圖的基礎概念(Day 22)和進階節點操作(Day 23)。今天我們要學習 Koog 框架最強大的功能之一:子圖(Subgraph),它讓我們能夠以模組化的方式建構複雜的 AI 應用
什麼是子圖?
子圖的概念
子圖(Subgraph)就像是程式設計中的函數或模組,它將一組相關的節點和邊封裝起來,形成一個可重用的功能單元
想像一下,如果策略圖是一個大型工廠,那麼子圖就是工廠內的不同專業廠房
- 研究廠房:負責資料收集和分析
- 設計廠房:負責方案規劃和設計
- 生產廠房:負責實際執行和製造
- 品管廠房:負責品質檢查和驗證
每個廠房都有自己的專業設備(工具)和作業流程(節點和邊),但它們可以協同工作來完成整個生產流程
為什麼需要子圖?
- 模組化設計:將複雜邏輯分解成小單元
- 重用性:相同的子圖可以在不同地方使用
- 維護性:修改某個功能只需要調整對應的子圖
- 權限控制:不同子圖可以存取不同的工具集
- 團隊協作:不同開發者可以負責不同的子圖
子圖概念架構圖
flowchart TD
A[主策略圖開始] --> B[資料收集子圖]
B --> C[分析處理子圖]
C --> D[決策執行子圖]
D --> E[主策略圖結束]
subgraph SG1 [資料收集子圖]
F[網路搜尋節點] --> G[資料庫查詢節點]
G --> H[資料整合節點]
end
subgraph SG2 [分析處理子圖]
I[趨勢分析節點] --> J[風險評估節點]
J --> K[報告生成節點]
end
subgraph SG3 [決策執行子圖]
L[策略制定節點] --> M[任務分配節點]
M --> N[執行監控節點]
end
B -.-> SG1
C -.-> SG2
D -.-> SG3
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#f3e5f5
style D fill:#f3e5f5
style E fill:#e8f5e8
style F fill:#fff3e0
style G fill:#fff3e0
style H fill:#fff3e0
style I fill:#fff3e0
style J fill:#fff3e0
style K fill:#fff3e0
style L fill:#fff3e0
style M fill:#fff3e0
style N fill:#fff3e0
style SG1 fill:#f8f9fa,stroke:#6c757d
style SG2 fill:#f8f9fa,stroke:#6c757d
style SG3 fill:#f8f9fa,stroke:#6c757d
因為流程圖太大張了,在網頁裡面可能不好觀看,大家可以用上面的
mermaid
去 mermaid.live 看
這個架構圖展示了子圖的核心概念
- 主策略圖:負責整體流程控制和子圖間的協調
- 資料收集子圖:封裝所有與資料獲取相關的節點和邏輯
- 分析處理子圖:專門處理資料分析和報告生成
- 決策執行子圖:負責根據分析結果制定和執行策略
每個子圖都有自己的內部流程,但對外部來說就像是一個單一的功能節點,這就是模組化的精髓
子圖的基本語法
建立基本子圖
val strategy = strategy<String, String>("main_strategy") {
// 定義子圖
val dataProcessingSubgraph by subgraph<String, String>("data_processing") {
// 在這裡定義子圖內的節點和邊
val processNode by node<String, String>("process") { input ->
"處理後的資料:$input"
}
edge(nodeStart forwardTo processNode)
edge(processNode forwardTo nodeFinish)
}
// 使用子圖
edge(nodeStart forwardTo dataProcessingSubgraph)
edge(dataProcessingSubgraph forwardTo nodeFinish)
}
子圖的工具隔離
每個子圖可以指定自己可用的工具集,提供更好的安全性和控制
val researchSubgraph by subgraph<String, String>(
name = "research",
tools = listOf(WebSearchTool(), DatabaseQueryTool()) // 只能使用這些工具
) {
// 子圖內的邏輯
}
子圖間的串接
使用 then
運算子可以簡潔地串接多個子圖
nodeStart then researchSubgraph then analysisSubgraph then executionSubgraph then nodeFinish
特殊子圖類型
subgraphWithTask:任務導向子圖
這是一個預定義的子圖模式,專門用於執行特定任務,它封裝了 LLM 互動和工具使用的完整流程
val analysisTask by subgraphWithTask<String>(
tools = listOf(CalculatorTool(), ChartTool()),
llmModel = OpenAIModels.Chat.GPT4o
) { data ->
"""
請分析以下資料並產生報告:
$data
需要包含:
1. 資料摘要
2. 趨勢分析
3. 建議事項
"""
}
subgraphWithVerification:帶驗證的子圖
這種子圖會自動在任務完成後執行驗證步驟,確保輸出品質符合要求
val codeReviewTask by subgraphWithVerification<String>(
tools = listOf(LintTool(), TestTool(), SecurityScanTool()),
llmModel = OpenAIModels.Chat.GPT4o
) { codeToReview ->
"""
請檢查以下程式碼:
$codeToReview
檢查項目:
1. 語法正確性
2. 程式碼風格
3. 安全性問題
4. 效能考量
"""
}
市場調查專案系統實作範例
讓我們建立一個市場調查專案系統,來說明如何使用子圖
系統整體流程圖
flowchart TD
A[專案開始] --> B[研究階段子圖]
B --> C[規劃階段子圖]
C --> D[執行階段子圖]
D --> E[專案完成]
subgraph SG1 [研究階段子圖]
direction TB
F[初始化研究提示] --> G[LLM 研究請求]
G --> H{回應類型}
H -->|工具呼叫| I[執行研究工具]
H -->|助手訊息| K[研究完成]
I --> J[處理工具結果]
J --> L{繼續研究?}
L -->|是| I
L -->|否| K
K --> M[研究結果輸出]
end
subgraph SG2 [規劃階段子圖]
direction TB
N[接收研究結果] --> O[制定執行計劃]
O --> P[生成任務清單]
P --> Q[計劃輸出]
end
subgraph SG3 [執行階段子圖]
direction TB
R[初始化執行提示] --> S[LLM 執行請求]
S --> T{回應類型}
T -->|工具呼叫| U[執行專案工具]
T -->|完成訊息| X[生成最終報告]
U --> V[處理執行結果]
V --> W{繼續執行?}
W -->|是| U
W -->|否| X
X --> Y[專案報告輸出]
end
B -.-> SG1
C -.-> SG2
D -.-> SG3
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#f3e5f5
style D fill:#f3e5f5
style E fill:#e8f5e8
style F fill:#fff3e0
style G fill:#f3e5f5
style H fill:#fff3e0
style I fill:#f3e5f5
style J fill:#f3e5f5
style K fill:#e8f5e8
style L fill:#fff3e0
style M fill:#e8f5e8
style N fill:#fff3e0
style O fill:#f3e5f5
style P fill:#f3e5f5
style Q fill:#e8f5e8
style R fill:#fff3e0
style S fill:#f3e5f5
style T fill:#fff3e0
style U fill:#f3e5f5
style V fill:#f3e5f5
style W fill:#fff3e0
style X fill:#f3e5f5
style Y fill:#e8f5e8
style SG1 fill:#f8f9fa,stroke:#6c757d,stroke-width:2px
style SG2 fill:#f8f9fa,stroke:#6c757d,stroke-width:2px
style SG3 fill:#f8f9fa,stroke:#6c757d,stroke-width:2px
因為流程圖太大張了,在網頁裡面可能不好觀看,大家可以用上面的
mermaid
去 mermaid.live 看
這個流程圖展示了研究-規劃-執行系統的完整架構
研究階段子圖 (Research Phase)
- 工具集:WebSearchTool、DataAnalysisTool
- 流程:循環使用工具進行市場研究,收集並分析相關資料
- 輸出:整合後的研究結果和洞察
規劃階段子圖 (Planning Phase)
- 工具集:純 LLM 思考,不需外部工具
- 流程:基於研究結果制定具體的執行計劃
- 輸出:結構化的任務清單,包含優先級
執行階段子圖 (Execution Phase)
- 工具集:ProjectExecutionTool、QualityCheckTool
- 流程:按優先級執行任務並進行品質檢查
- 輸出:完整的專案執行報告
每個階段都是獨立的子圖,具有明確的職責和工具權限,資料在子圖間順序傳遞和轉換
新增市場調查專案的相關工具
class WebSearchTool : SimpleTool<WebSearchTool.Args>() {
@Serializable
class Args(val query: String) : ToolArgs
override val argsSerializer = Args.serializer()
override val descriptor = ToolDescriptor("web_search", "搜尋網路資訊")
override suspend fun doExecute(args: Args): String {
// 模擬網路搜尋
println("🔍 搜尋:${args.query}")
return when {
args.query.contains("市場") -> "市場調查顯示:產品需求正在增長,競爭對手較少"
args.query.contains("技術") -> "技術分析:使用 Kotlin 和 AI 整合具有良好前景"
args.query.contains("成本") -> "成本分析:預估開發成本約 100 萬,維護成本每月 5 萬"
else -> "找到相關資訊:${args.query} 的搜尋結果"
}
}
}
class DataAnalysisTool : SimpleTool<DataAnalysisTool.Args>() {
@Serializable
class Args(val data: String, val analysisType: String) : ToolArgs
override val argsSerializer = Args.serializer()
override val descriptor = ToolDescriptor("data_analysis", "資料分析工具")
override suspend fun doExecute(args: Args): String {
println("📊 分析資料:${args.analysisType}")
return when (args.analysisType) {
"趨勢" -> "趨勢分析:市場呈現上升趨勢,成長率約 15%"
"風險" -> "風險評估:主要風險為技術變化快速,建議持續關注"
"預測" -> "預測結果:預計 6 個月內可完成開發,12 個月內回收成本"
else -> "分析完成:${args.data} 的 ${args.analysisType} 分析結果"
}
}
}
class ProjectExecutionTool : SimpleTool<ProjectExecutionTool.Args>() {
@Serializable
class Args(val taskDescription: String, val priority: String) : ToolArgs
override val argsSerializer = Args.serializer()
override val descriptor = ToolDescriptor(
"execute_task",
"執行專案任務。當需要實際執行計劃中的具體任務時使用此工具,例如:建立團隊、開發功能、設計架構等"
)
override suspend fun doExecute(args: Args): String {
println("⚡ 執行任務:${args.taskDescription}(優先級:${args.priority})")
return "任務已開始執行:${args.taskDescription},預計完成時間根據優先級${args.priority}進行排程"
}
}
class QualityCheckTool : SimpleTool<QualityCheckTool.Args>() {
@Serializable
class Args(val item: String, val criteria: String) : ToolArgs
override val argsSerializer = Args.serializer()
override val descriptor = ToolDescriptor(
"quality_check",
"進行品質檢查。每當任務執行完成後,必須使用此工具檢查輸出品質,例如:功能測試、程式碼審查、系統驗證等"
)
override suspend fun doExecute(args: Args): String {
println("✅ 品質檢查:${args.item}")
return "品質檢查通過:${args.item} 符合 ${args.criteria} 標準"
}
}
新增市場調查專案的相關流程
class ResearchPlanExecuteAgent {
private val toolRegistry = ToolRegistry {
tool(WebSearchTool())
tool(DataAnalysisTool())
tool(ProjectExecutionTool())
tool(QualityCheckTool())
}
private val agent = AIAgent(
executor = simpleOpenAIExecutor(ApiKeyManager.openAIApiKey!!),
toolRegistry = toolRegistry,
systemPrompt = """
你是一個專業的專案管理助手,能夠:
1. 進行市場和技術研究
2. 制定詳細的執行計劃
3. 協調專案執行
4. 進行品質控制
請使用正體中文回應。
""".trimIndent(),
llmModel = OpenAIModels.CostOptimized.GPT4_1Mini,
strategy = createResearchPlanExecuteStrategy(),
maxIterations = 200
)
private fun createResearchPlanExecuteStrategy() = strategy<String, String>("research_plan_execute") {
// 子圖一:研究階段
val researchSubgraph by subgraph<String, String>(
name = "research_phase",
tools = listOf(WebSearchTool(), DataAnalysisTool())
) {
println("🔬 進入研究階段")
// 初始化研究提示
val initResearchNode by node<String, Unit>("init_research") { projectDescription ->
llm.writeSession {
rewritePrompt {
prompt("research_prompt") {
system("""
你現在進入研究階段。請針對專案進行全面研究:
1. 使用 web_search 搜尋相關市場資訊、技術趨勢、競爭分析
2. 使用 data_analysis 分析收集到的資料
3. 進行多輪研究,確保資訊完整
4. 最後整理研究結果並提供深入洞察
請積極使用工具進行研究,不要只是回答。
""".trimIndent())
user("專案描述:$projectDescription")
}
}
}
}
val conductResearchNode by nodeLLMRequest("conduct_research")
val executeResearchToolsNode by nodeExecuteTool("execute_research_tools")
val processResearchResultsNode by nodeLLMSendToolResult("process_research_results")
// 研究階段流程 - 確保能循環執行工具
edge(nodeStart forwardTo initResearchNode)
edge(initResearchNode forwardTo conductResearchNode transformed { "開始進行專案研究" })
edge(conductResearchNode forwardTo executeResearchToolsNode onToolCall { true })
edge(executeResearchToolsNode forwardTo processResearchResultsNode)
// 允許多輪工具執行
edge(processResearchResultsNode forwardTo executeResearchToolsNode onToolCall { true })
// 只有當 LLM 明確表示研究完成時才結束
edge(conductResearchNode forwardTo nodeFinish onAssistantMessage { true })
edge(processResearchResultsNode forwardTo nodeFinish onAssistantMessage { true })
}
// 子圖二:規劃階段
val planningSubgraph by subgraph<String, String>(
name = "planning_phase",
tools = listOf() // 規劃階段主要靠 LLM 思考,不需要外部工具
) {
println("📋 進入規劃階段")
val createPlanNode by node<String, String>("create_plan") { researchResults ->
llm.writeSession {
rewritePrompt {
prompt("planning_prompt") {
system("""
基於研究結果,請制定具體的執行任務清單。
重要:你必須輸出結構化的任務清單,格式如下:
任務1:[具體任務描述]|優先級:[高/中/低]
任務2:[具體任務描述]|優先級:[高/中/低]
...
範例:
任務1:組建5人開發團隊並建立協作流程|優先級:高
任務2:設計系統架構和選擇技術棧|優先級:高
任務3:開發AI對話引擎核心模組|優先級:高
請基於研究結果產生3-5個具體可執行的任務。
""".trimIndent())
user("研究結果:$researchResults")
user("請輸出結構化的任務清單")
}
}
// 直接請求 LLM 回應並返回內容
val response = requestLLMWithoutTools()
response.content
}
}
// 簡化的規劃流程
edge(nodeStart forwardTo createPlanNode)
edge(createPlanNode forwardTo nodeFinish)
}
// 子圖三:執行階段
val executionSubgraph by subgraph<String, String>(
name = "execution_phase",
tools = listOf(ProjectExecutionTool(), QualityCheckTool())
) {
println("⚡ 進入執行階段")
val initExecutionNode by node<String, Unit>("init_execution") { executionPlan ->
llm.writeSession {
rewritePrompt {
prompt("execution_prompt") {
system("""
你現在是一個專案執行管理器。你的任務是解析任務清單並逐項執行。
重要規則:
1. 你必須使用 execute_task 工具來執行每一個任務
2. 每個任務執行完後,你必須使用 quality_check 工具進行品質檢查
3. 按優先級順序執行任務(高 → 中 → 低)
4. 執行完所有任務後,必須明確表示完成
執行流程:
解析任務清單 → 執行第一個任務 → 品質檢查 → 執行下一個任務 → ... → 完成所有任務
重要:當你執行完所有任務並進行品質檢查後,請回應文字訊息:
「所有任務執行完成,準備產生專案報告」
注意:完成時不要再呼叫任何工具,只需發送上述完成訊息。
""".trimIndent())
user("執行計劃:$executionPlan")
user("請立即解析上述任務清單,並開始執行第一個高優先級的任務。請使用 execute_task 工具。")
}
}
}
}
val executeTasksNode by nodeLLMRequest("execute_tasks")
val executeToolsNode by nodeExecuteTool("execute_tools")
val processExecutionResultsNode by nodeLLMSendToolResult("process_execution_results")
val generateFinalReportNode by node<String, String>("generate_final_report") { _ ->
llm.writeSession {
rewritePrompt {
prompt("final_report_prompt") {
system("""
請根據上述執行過程和結果,產生最終的專案執行報告。
報告格式:
# [專案名稱] - 專案執行報告
## 研究階段成果
[總結研究發現和洞察]
## 執行計劃與進度
[列出執行的任務和進度]
## 品質檢查結果
[總結品質檢查結果]
## 專案狀態總結
[整體進度和下一步建議]
請產生一份完整、專業的專案執行報告。
""".trimIndent())
user("請產生專案執行報告")
}
}
val response = requestLLMWithoutTools()
response.content
}
}
// 執行階段流程 - 恢復工具呼叫完整性
edge(nodeStart forwardTo initExecutionNode)
edge(initExecutionNode forwardTo executeTasksNode transformed { "立即解析任務清單並執行第一個任務" })
edge(executeTasksNode forwardTo executeToolsNode onToolCall { true })
edge(executeToolsNode forwardTo processExecutionResultsNode)
// 允許多輪工具執行(保持工具呼叫完整性)
edge(processExecutionResultsNode forwardTo executeToolsNode onToolCall { true })
// 改進的完成檢測:當 LLM 回應文字而非工具呼叫時進入報告生成
edge(executeTasksNode forwardTo generateFinalReportNode onAssistantMessage { response ->
response.content.contains("所有任務執行完成") ||
response.content.contains("準備產生報告") ||
response.content.contains("任務全部完成") ||
response.content.contains("執行完畢")
})
edge(processExecutionResultsNode forwardTo generateFinalReportNode onAssistantMessage { response ->
response.content.contains("所有任務執行完成") ||
response.content.contains("準備產生報告") ||
response.content.contains("任務全部完成") ||
response.content.contains("執行完畢")
})
// 最終報告輸出
edge(generateFinalReportNode forwardTo nodeFinish)
}
// 主流程:串接三個子圖
nodeStart then researchSubgraph then planningSubgraph then executionSubgraph then nodeFinish
}
suspend fun executeProject(projectDescription: String): String {
return agent.run(projectDescription)
}
}
使用市場調查專案範例
// 執行範例
suspend fun main() {
val projectManager = ResearchPlanExecuteAgent()
println("=== 研究-規劃-執行系統演示 ===\n")
val projectDescription = """
專案名稱:AI 驅動的客戶服務平台
目標:
- 開發一個智慧客服系統
- 支援多語言對話
- 整合現有 CRM 系統
- 提供即時分析報告
預算:200 萬台幣
時程:12 個月
""".trimIndent()
println("📝 專案需求:")
println(projectDescription)
println("\n" + "=".repeat(60) + "\n")
try {
val result = projectManager.executeProject(projectDescription)
println("🎯 專案管理結果:")
println(result)
} catch (e: Exception) {
println("❌ 專案執行失敗:${e.message}")
}
}
執行 AI 回應內容
執行的時候要注意,很容易會遇到預設的 iterations 50 次不夠的問題,我是先設定 maxIterations 為 200
🔬 進入研究階段
📋 進入規劃階段
⚡ 進入執行階段
=== 研究-規劃-執行系統演示 ===
📝 專案需求:
專案名稱:AI 驅動的客戶服務平台
目標:
- 開發一個智慧客服系統
- 支援多語言對話
- 整合現有 CRM 系統
- 提供即時分析報告
預算:200 萬台幣
時程:12 個月
============================================================
🔍 搜尋:智慧客服系統 市場趨勢 競爭分析 多語言對話 CRM整合 即時分析報告
🔍 搜尋:AI 智慧客服平台 多語言技術 CRM整合方案 即時分析技術 最新趨勢
🔍 搜尋:智慧客服系統 競爭者分析 CRM整合 成功案例
📊 分析資料:summary
⚡ 執行任務:進一步調查並分析目標市場中客戶對智慧客服系統的具體需求和痛點(優先級:高)
✅ 品質檢查:任務1執行結果
⚡ 執行任務:研究並評估自然語言處理(NLP)技術及多語言支援方案的技術可行性(優先級:高)
✅ 品質檢查:任務2執行結果
⚡ 執行任務:深入分析主要競爭對手的產品特性和用戶反饋,提取可借鑑的設計與功能方案(優先級:中)
✅ 品質檢查:任務3執行結果
⚡ 執行任務:制定以Kotlin為核心的系統開發技術方案,包括系統架構設計與技術棧選擇(優先級:中)
✅ 品質檢查:任務4執行結果
⚡ 執行任務:規劃智慧客服系統與現有CRM系統的整合方案,確保高度靈活性與用戶友好介面設計(優先級:中)
✅ 品質檢查:任務5執行結果
🎯 專案管理結果:
# 專案名稱 - 專案執行報告
## 研究階段成果
本階段深入分析了專案相關市場趨勢與使用者需求,明確界定目標群體及其痛點,確立了專案核心目標與價值主張。研究結果為後續設計與開發提供了堅實依據,確保專案方向貼近用戶期望且具市場競爭力。
## 執行計劃與進度
- 完成需求分析與功能規劃
- 開展系統設計與架構建置
- 實施第一階段模組開發與內部測試
- 持續進行產品優化與用戶體驗改進
目前所有計劃任務依既定時間表進行,關鍵節點均如期完成。
## 品質檢查結果
品質檢查涵蓋程式碼審核、功能測試、效能評估與安全性驗證,結果顯示系統穩定性高,功能符合設計規範,且用戶界面友好易用。少數輕微問題已追蹤並進入修復流程,整體品質達標。
## 專案狀態總結
專案目前進度良好,關鍵里程碑均已達成,團隊協作順暢。建議持續關注用戶反饋,針對潛在風險提前制定應對策略,確保後續開發與上線階段順利推進。下一步將重點放在功能完善與性能優化,全面提升產品競爭力。
常見執行問題與解決方案
在實際執行研究-規劃-執行系統時,你可能會遇到一些常見問題
執行階段返回 JSON 而非報告
現象:執行結果顯示類似 {"tool_call_id":"call_FinalReport_001",...}
的 JSON 格式
原因:LLM 在完成所有任務後,仍然嘗試呼叫工具而不是進入報告生成節點
解決方案
- 強化提示詞:明確告訴 LLM 完成後發送文字訊息而非工具呼叫
- 保持工具呼叫完整性:確保每個工具呼叫都有對應的回應
- 使用
onAssistantMessage
條件:檢測 LLM 的文字回應來判斷完成
// 關鍵修正:使用文字回應檢測完成
edge(processExecutionResultsNode forwardTo generateFinalReportNode onAssistantMessage { response ->
response.content.contains("所有任務執行完成") ||
response.content.contains("準備產生專案報告")
})
工具執行無限循環
現象:系統一直在執行工具呼叫,無法結束
原因:邊定義中缺少明確的退出條件
解決方案:
- 設置最大迭代次數:在 agent 配置中設置
maxIterations
- 添加條件檢查:使用
onCondition
來控制循環退出 - 使用狀態追蹤:在提示詞中添加狀態管理
子圖間資料傳遞問題
現象:後續子圖無法獲得前一個子圖的完整結果
原因:子圖間的資料轉換不正確
解決方案:
- 使用
transformed
函數:確保資料格式正確 - 添加中間節點:用於資料格式化和驗證
- 檢查資料類型:確保輸入輸出類型匹配
edge(researchSubgraph forwardTo planningSubgraph transformed { researchResult ->
"研究結果:$researchResult" // 確保格式正確
})
子圖最佳實踐與設計原則
- 單一責任:每個子圖負責一個明確的功能領域
- 鬆散耦合:子圖間通過明確的介面互動
- 高內聚:子圖內部的節點緊密相關
- 可測試性:每個子圖都可以獨立測試
總結
今天我們學習了 Koog 框架的子圖與模組化設計
- 子圖概念:將複雜邏輯封裝成可重複使用的模組
- 基本語法:subgraph 定義、工具隔離、子圖串接
- 特殊類型:subgraphWithTask、subgraphWithVerification
- 實際應用:研究-規劃-執行系統的完整實作
- 檢查點整合:如何在子圖中使用檢查點系統
子圖是構建企業級 AI 應用的重要工具,它讓我們能夠以工程化的方式管理複雜性,提升系統的可維護性和可擴展性
透過這三篇策略圖系列文章的學習,你現在已經掌握了從基礎到進階的策略圖設計技巧。在接下來的檢查點系統文章中,你將學習如何讓這些複雜的工作流程具備容錯和恢復能力
參考資源
支持創作
如果這篇文章對您有幫助,歡迎透過 贊助連結 支持我持續創作優質內容。您的支持是我前進的動力!
圖片來源:AI 產生