前陣子學會用 Agent Framework 開發 Agent 或聊天程式,接著來試些有趣的應用。上回玩過用 AIChatWeb 寫出可傳圖檔的 AI 聊天室功能,這回我想試試為 LINE 寫個可以傳圖的 AI 聊天機器人。

類似的題目之前做過,像是 ChatGPT 翻譯鳳梨酥 LINE Bot,原則上就是用 David 老師的 LineBotSDK 處理與 LINE 通訊的部分再串接語言模型的聊天 API。MS Agent Framework 有 AgentSession 幫忙處理聊天歷史記錄,不必自己保存及打包歷史對話內容,程式寫起來輕鬆不少。甚至還可自訂儲存機制(預設是存在記憶體)、設定壓縮策略(https://learn.microsoft.com/zh-tw/agent-framework/agents/conversations/compaction?pivots=programming-language-csharp)(歷史對話過多時進行摘要濃縮,以免超過上下文窗口長度上限,避免 AI 抓錯重點,另一方面也減少 Token 省點錢),是個進行攻退可守的彈性架構。

程式的主架構是用 OpenAI 或 AOAI 的 API Url 及 Key 建立 ChatClient,傳入 Prompt 提示、工具函式清單建立 AIAgent。另一方面,程式會執行 ASP.NET Minimal API 跑網站,並用 MapPost() 加入一段訊息處理邏輯,並向 LINE 註冊這個 URL。當 LINE 聊天室有人發言,LINE 會呼叫這個 Callback 網址並以 JSON 格式傳入訊息內容,程式從 JSON 中可取得使用者訊息文字或上傳的圖片,將文字及圖片當成輸入參數呼叫 AIAgent.RunAsync() 取得 AI 模型的回應。但這裡有個重點是,每次呼叫 AI 模型時,必須帶入先前的交談內容,不然模型不會接續先前的話題。例如:你問天氣如何?AI 問你想知道哪裡的天氣,你回答"台北"後第二次呼叫模型,若沒帶入先前天氣的討論,AI 看到沒頭沒腦的台北二字,哪知道你在問天氣。每次要記得帶入先前的討論挺煩人,Agent Framework 提供了一個 AgentSession 物件,我們可以用 AIAgent.CreateSessionAsync() 建立,每次呼叫 .RunAsync() 時當成參數帶入,AgentSession 將自動幫忙保留交談記錄,我們只需要每次呼叫 AIAgent.RunAsync() 時傳入同一個 AgentSession 就好了。

但還有兩個問題要解決。第一,不管 ChatGPT/Gemini/Claude/Copliot 都有個開啟一段新對話的功能,對映到 LINE 聊天,相當於丟掉先前的交談記錄,新建一個 AgentSession 重新開始,因此要設計像是 /clear 之類的指令觸發重建 Session 的動作。第二是 LINE Bot 可能同時跟多人交談,每個人要有一個專屬的 AgentSession 不能共用,生命週期也不同。我的解法是設計一個 SessionState 類別,除了 AgentSession 外再加上暫存圖檔(因應先傳圖,之後才說需求的情境)、清除重建、寫 Log 等功能。SessionState 長這樣:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;

namespace AgentFxLineBot
{
    public class SessionState
    {
        NLog.Logger logger = null!;
        public string SessionId { get; set; } = "NA";
        public string LineUserId { get; set; } = "NA";

        public Microsoft.Agents.AI.AgentSession Session { get; set; } = null!;
        public SessionState(string lineUserId, Microsoft.Agents.AI.AgentSession session)
        {
            Console.WriteLine($"建立新 Session: {lineUserId}");
            LineUserId = lineUserId;
            Reset(session);
        }
        public void Reset(Microsoft.Agents.AI.AgentSession session)
        {
            SessionId = $"{LineUserId}-{DateTime.Now:yyyyMMdd-HHmmss}-{Guid.NewGuid().ToString().Substring(0, 4)}";
            logger = NLog.LogManager.GetLogger(SessionId);
            Session = session;
            Images.Clear();
        }
        public List<AIContent> Images = new List<AIContent>();
        public void LogInput(string msg) => logger.Debug("INPUT\n" + msg);
        public void LogOutput(string msg) => logger.Debug("OUTPUT\n" + msg);

    }
}

主程式不長,不到 150 行搞定:

using Microsoft.Extensions.Configuration;
using Azure.AI.OpenAI;
using System.ClientModel;
using System.ComponentModel;
using Microsoft.Extensions.AI;
using isRock.LineBot;
using System.Text.RegularExpressions;
using System.Collections.Concurrent;
using AgentFxLineBot;
using OpenAI.Chat;
using MSChatMessage = Microsoft.Extensions.AI.ChatMessage;
using AngleSharp;
using AngleSharp.Io;


var builder = WebApplication.CreateBuilder(args);

const string modelName = "gpt-5.2-chat";
var chanAccToken = builder.Configuration["LineBotToken"] ?? throw new Exception("No LineBotToken setting");

var azureChatClient = new AzureOpenAIClient(
        new Uri(builder.Configuration["AoaiEndPoint"] ?? throw new Exception("No AoaiEndPoint setting")),
        new ApiKeyCredential(builder.Configuration["AoaiApiKey"] ?? throw new Exception("No AoaiApiKey setting"))
    ).GetChatClient(modelName);


var agent = azureChatClient
    .AsAIAgent(
        instructions:
            """
            你是 LINE 聊天助理,負責完成以下任務:

            - 若使用者提供 URL,依其指示處理該網頁內容
            - 若使用者提供圖片,依其指示分析圖片內容

            ## 執行說明

            - 使用者除 URL 及圖片外需說明需求,請先確認需求,再進行分析
            - 使用 ReadWebPage 工具取得網頁內容
            - 不要使用 Emoji 與 Markdown
            """,
        tools: [
            AIFunctionFactory.Create(ReadWebPageContent)
        ]
    );

string helpMsg =
"""
MS Agent Framework 版 LINE 聊天機器人 PoC
====
使用 gpt-5.2-chat,支援讀取網頁與圖片解析

## 指令
/help 顯示此說明
/clear 清除對話記憶
""";

var app = builder.Build();

var bot = new isRock.LineBot.Bot(chanAccToken);

app.MapGet("/", () => "Hello World!");

var sessionPool = new ConcurrentDictionary<string, SessionState>();

app.MapPost("/MsgCallback", async (HttpContext context) =>
{
    var baseUri = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.PathBase}";
    var body = context.Request.Body;
    using var reader = new StreamReader(body);
    var json = await reader.ReadToEndAsync();
    var msg = Utility.Parsing(json);
    foreach (var evt in msg.events)
    {
        if (evt.type == "message")
        {
            if (evt.source.type != "user")
            {
                // 不支援群組聊天室,僅支援與單人對話
                continue;
            }
            var lineUserId = evt.source.userId;
            var sessionState = sessionPool.GetOrAdd(lineUserId, _ => new SessionState(lineUserId, agent.CreateSessionAsync().Result));
            if (evt.message.type == "image")
            {
                var image = await AgentFxLineBot.LineBotUtils.GetUserUploadedContentAsync(evt.message.id, chanAccToken);
                var mediaType = image.MimeType;
                var data = new DataContent(image.Content, mediaType);
                data.AdditionalProperties = new() { ["name"] = (object?)image.Id + "." + mediaType.Split('/').Last() };
                Console.WriteLine($"Received image with media type: {mediaType}");
                sessionState.Images.Add(data);
            }
            else if (evt.message.type == "text")
            {
                var text = evt.message.text;
                switch (text)
                {
                    case "/help":
                        bot.ReplyMessage(evt.replyToken, helpMsg);
                        break;
                    case "/clear":
                        sessionState.Reset(agent.CreateSessionAsync().Result);
                        bot.ReplyMessage(evt.replyToken, "已重設交談階段");
                        break;
                    default:
                        sessionState.LogInput(text);
                        string outMsg;
                        try
                        {
                            var chatMsg = new MSChatMessage(ChatRole.User, text);
                            if (sessionState.Images.Any())
                            {
                                chatMsg = new MSChatMessage(ChatRole.User, new List<AIContent>() { new TextContent(text) }.Concat(sessionState.Images).ToList());
                                sessionState.Images.Clear();
                            }
                            var res = await agent.RunAsync(chatMsg, sessionState.Session);
                            outMsg = res.Text;
                            sessionState.LogOutput(outMsg);
                        }
                        catch (Exception ex)
                        {
                            sessionState.LogOutput("Error: " + ex.Message);
                            outMsg = $"發生錯誤:{ex.Message}";
                        }
                        bot.ReplyMessage(evt.replyToken, outMsg);
                        break;
                }
            }
        }
    }
    await context.Response.WriteAsync("OK");
});
app.UsePathBase("/line-bot");
app.Run();

[Description("輸入 URL 讀取網頁 HTML 內容")]
static async Task<string> ReadWebPageContent([Description("網頁 URL")] string url)
{
    Console.WriteLine($"讀取網頁內容: {url}");
    try
    {
        var config = Configuration.Default.WithDefaultLoader();
        var context = BrowsingContext.New(config);
        var document = await context.OpenAsync(url);
        return document?.Body?.TextContent ?? string.Empty;
    }
    catch (Exception ex)
    {
        return $"無法讀取網頁內容: {ex.Message}";
    }
}

中間有個小眉角,isRock.LineBot SDK 用 GetUserUploadedContent() 取圖檔時只會拿到 byte[],但後續要轉 DataContent 交給模型時需指定 MIME Type,為此我寫了個函式自己呼叫 LINE WebApi,在下載時一併由 Response Header 取得 MIME Type。

就醬,一個可以讀取網頁總結內容,還能上傳圖檔分析的 AI 聊天 LINE 機器人就寫好了,核心採用 GPT-5.2-Chat,靠 Agent Framework 框架可以很容易加掛各種工具、MCP、Agent 無限擴充能力,化身強大的 AI Agent。這種 AI 助理的能力自然不能跟小龍蝦相提並論,那就叫它黑殼蝦吧~

程式碼我放上 Github 了,需要的同學請自取。

Demonstrates building a LINE AI chatbot with Microsoft Agent Framework, supporting images and web reading. Uses AgentSession to manage per-user conversation history, reset commands, and tool integration, showcasing a scalable, extensible design powered by GPT‑5.2‑Chat.


Comments

Be the first to post a comment

Post a comment