iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0
生成式 AI

當 .NET 遇見 AI Agents:用 Semantic Kernel × MCP 打造智慧協作應用系列 第 10

Day 10:來了!! Single Agent 實戰 - 對帳稽核 Agent

  • 分享至 

  • xImage
  •  

延續上一篇文章,本篇將來實作一個 Single Agent 範例,目標是打造一個「應付帳款三方對帳(PO/收貨單/發票)稽核 Agent」,流程是把「採購單(PO)」、「收貨單(GRN / 入庫單)」與「供應商發票(Invoice)」做三方比對,若金額、數量、品項或稅額不一致就要標記為例外;若一致則自動進入待付款流程。這樣的 Agent 能夠協助企業自動化處理對帳工作,提升效率並減少人為錯誤。

這個Agent 範例會使用 ChatCompletionAgent 結合 OpenAI 的 GPT-4.1 模型,加上財務相關的工具掛載到 Agent,讓 Agent 能夠自主決策根據需求自動調用工具(如:查詢發票、比對金額等),實現對帳稽核的自動化流程。

實作步驟

先確保安裝以下套件:

  • Microsoft.SemanticKernel
  • Microsoft.SemanticKernel.Agents.Abstractions
  • Microsoft.SemanticKernel.Agents.Core

建立 AccountsPayablePlugin 類別,實作應付帳款相關工具

這個 Plugin 提供了多個 function 來模擬取得採購單、收貨單與發票資料、稽核檢驗等作業。

  • 屬性:模擬不同幣別的匯率
private const string BaseCurrency = "TWD";
  • function:依據 PO 編號取得採購單資料function
[KernelFunction, Description("依據 PO 編號取得採購單資料")]
public Task<POData> GetPurchaseOrderAsync(
    [Description("採購單編號(例如:PO-202509-001)")] string poNumber)
{
    var po = new POData
    {
        PONumber = poNumber,
        SupplierCode = "SUP-001",
        Currency = BaseCurrency,
        TaxRatePercent = 5, // 5%
        Lines = new List<LineItem>
        {
            new() {Sku="A-100", Qty=100, UnitPrice=300},  // 單價整數 TWD
            new() {Sku="B-200", Qty=50,  UnitPrice=1200}
        }
    };
    return Task.FromResult(po);
}
  • function:依據入庫單號取得收貨單(GRN/入庫單)資料
[KernelFunction, Description("依據入庫單號取得收貨單(GRN/入庫單)資料")]
public Task<GRNData> GetGoodsReceiptAsync(
    [Description("入庫單號(例如:GRN-7788)")] string grnNumber)
{
    var grn = new GRNData
    {
        GRNNumber = grnNumber,
        Currency = BaseCurrency,
        Lines = new List<LineItem>
        {
            new() {Sku="A-100", Qty=99, UnitPrice=300},   // 入庫數量 99(整數)
            new() {Sku="B-200", Qty=50, UnitPrice=1200}
        }
    };
    return Task.FromResult(grn);
}
  • function:依據發票號碼取得發票資料
[KernelFunction, Description("依據發票號碼取得發票資料")]
public Task<InvoiceData> GetInvoiceAsync(
    [Description("供應商發票號碼(例如:INV-5566)")] string invoiceNumber)
{
    var inv = new InvoiceData
    {
        InvoiceNumber = invoiceNumber,
        SupplierCode = "SUP-001",
        Currency = BaseCurrency,
        TaxRatePercent = 5, // 5%
        Lines = new List<LineItem>
        {
            new() {Sku="A-100", Qty=100, UnitPrice=300},
            new() {Sku="B-200", Qty=50,  UnitPrice=1200}
        }
    };
    return Task.FromResult(inv);
}
  • function:執行 PO/GRN/Invoice 三方對帳並回傳差異與結論
    這個 function 會比對三張單據的品項、數量、金額與稅率,並根據設定的容差百分比來判斷是否一致,最後回傳對帳結果,是整個 Agent 的核心功能。
[KernelFunction, Description("執行 PO/GRN/Invoice 三方對帳並回傳差異與結論")]
public Task<MatchResult> MatchThreeWayAsync(
    [Description("採購單編號(PO Number)")] string poNumber,
    [Description("入庫單號(GRN Number)")] string grnNumber,
    [Description("發票號碼(Invoice Number)")] string invoiceNumber,
    [Description("數量容差百分比(整數或小數,預設 1.0 代表 1%)")] double qtyTolerancePercent = 1.0,
    [Description("金額容差百分比(整數或小數,預設 0.5 代表 0.5%)")] double amountTolerancePercent = 0.5)
{
    var po = GetPurchaseOrderAsync(poNumber).Result;
    var grn = GetGoodsReceiptAsync(grnNumber).Result;
    var inv = GetInvoiceAsync(invoiceNumber).Result;

    var diffs = new List<DiffItem>();

    // 以 SKU 對齊
    var skuSet = po.Lines.Select(l => l.Sku)
        .Union(grn.Lines.Select(l => l.Sku))
        .Union(inv.Lines.Select(l => l.Sku))
        .Distinct();

    foreach (var sku in skuSet)
    {
        var poLine = po.Lines.FirstOrDefault(l => l.Sku == sku);
        var grnLine = grn.Lines.FirstOrDefault(l => l.Sku == sku);
        var invLine = inv.Lines.FirstOrDefault(l => l.Sku == sku);

        if (poLine is null || grnLine is null || invLine is null)
        {
            diffs.Add(new DiffItem(sku, "MISSING_LINE", "某張單缺少此 SKU"));
            continue;
        }

        // 數量差異(以 PO 數量為基準百分比)
        var qtyDiff = (double)Math.Abs(invLine.Qty - grnLine.Qty) / Math.Max(1, poLine.Qty) * 100.0;
        if (qtyDiff > qtyTolerancePercent)
        {
            diffs.Add(new DiffItem(sku, "QTY_DIFF",
                $"數量差異 {qtyDiff:F2}% 超過容差 {qtyTolerancePercent}%"));
        }

        // 金額(皆為 TWD,整數)
        var poAmt = poLine.Qty * poLine.UnitPrice;
        var grnAmt = grnLine.Qty * grnLine.UnitPrice;
        var invAmt = invLine.Qty * invLine.UnitPrice;

        var amtDiffVsPO = (double)Math.Abs(invAmt - poAmt) / Math.Max(1, poAmt) * 100.0;
        if (amtDiffVsPO > amountTolerancePercent)
        {
            diffs.Add(new DiffItem(sku, "AMOUNT_DIFF",
                $"金額 vs PO 差異 {amtDiffVsPO:F2}% 超過容差 {amountTolerancePercent}%"));
        }
    }

    // 稅率檢核(整數百分比比較)
    if (inv.TaxRatePercent != po.TaxRatePercent)
    {
        diffs.Add(new DiffItem("*", "TAX_MISMATCH",
            $"發票稅率 {inv.TaxRatePercent}% 與 PO 稅率 {po.TaxRatePercent}% 不一致"));
    }

    var passed = diffs.Count == 0;
    var result = new MatchResult
    {
        Passed = passed,
        Summary = passed ? "三方對帳一致,可進入待付款流程" : "發現例外,需建立例外事件單",
        Differences = diffs
    };

    return Task.FromResult(result);
}
  • function:為通過三方對帳的單據建立待付款任務
    這裡會模擬建立一個待付款任務,回傳一個模擬的付款任務編號。實務上可以整合企業的 ERP 或財務系統來建立真正的付款任務。
[KernelFunction, Description("為通過三方對帳的單據建立待付款任務")]
public Task<string> CreatePaymentTaskAsync(
    [Description("採購單編號(PO Number)")] string poNumber,
    [Description("發票號碼(Invoice Number)")] string invoiceNumber,
    [Description("建議付款日(yyyy-MM-dd),用於折扣期或付款條件計算")] string suggestedPayDate)
{
    return Task.FromResult($"PAY-{DateTime.UtcNow:yyyyMMddHHmmss}");
}
  • function:為三方對帳的差異建立例外事件單(未通過對帳的情況下使用)
    這裡會替未通過檢核的對帳資料,模擬建立一個例外事件單,回傳一個模擬的事件單編號。實務上可以整合企業的事件管理系統來建立真正的事件單。
[KernelFunction, Description("為通過三方對帳的單據建立待付款任務")]
public Task<string> CreatePaymentTaskAsync(
    [Description("採購單編號(PO Number)")] string poNumber,
    [Description("發票號碼(Invoice Number)")] string invoiceNumber,
    [Description("建議付款日(yyyy-MM-dd),用於折扣期或付款條件計算")] string suggestedPayDate)
{
    return Task.FromResult($"PAY-{DateTime.UtcNow:yyyyMMddHHmmss}");
}
  • function 會使用到的資料結構
public record LineItem
{
    public string Sku { get; set; } = "";
    public int Qty { get; set; }               // 整數
    public int UnitPrice { get; set; }         // 整數(TWD)
}

public record POData
{
    public string PONumber { get; set; } = "";
    public string SupplierCode { get; set; } = "";
    public string Currency { get; set; } = "TWD";
    public int TaxRatePercent { get; set; }    // 整數百分比,例如 5 代表 5%
    public List<LineItem> Lines { get; set; } = new();
}

public record GRNData
{
    public string GRNNumber { get; set; } = "";
    public string Currency { get; set; } = "TWD";
    public List<LineItem> Lines { get; set; } = new();
}

public record InvoiceData
{
    public string InvoiceNumber { get; set; } = "";
    public string SupplierCode { get; set; } = "";
    public string Currency { get; set; } = "TWD";
    public int TaxRatePercent { get; set; }    // 整數百分比
    public List<LineItem> Lines { get; set; } = new();
}

public record DiffItem(string Sku, string Type, string Detail);

public record MatchResult
{
    public bool Passed { get; set; }
    public string Summary { get; set; } = "";
    public List<DiffItem> Differences { get; set; } = new();
}

建立 AuditAgent 類別,實作對帳稽核 Agent

準備好 AccountsPayablePlugin 後,就可以建立 AuditAgent 類別,實作對帳稽核 Agent。

  • 建立 Kernel 並掛載 AccountsPayablePlugin
var kernel = Kernel.CreateBuilder()
                .AddOpenAIChatCompletion(
                    apiKey: Config.OpenAI_ApiKey,
                    modelId: Config.ModelId)
                .Build();

// 註冊「應付帳款三方對帳」工具
kernel.Plugins.AddFromType<AccountsPayablePlugin>();
  • 建立 ChatCompletionAgent
    賦予 Instructions Prompt 並設定 Function Choice Behavior 為 Auto,讓 Agent 能夠根據使用者輸入自動挑選並呼叫適當的工具來完成任務。
var agent = new ChatCompletionAgent
{
    Name = "AuditAgent",
    Description = "自動執行 PO/收貨單/發票 的三方對帳與例外處理的 AI 稽核助理",
    Instructions = """
                    你是一位嚴謹的『應付帳款三方對帳稽核』AI 助理,負責協助財務人員進行以下任務:
                    1) 三方對帳:對比 PO(採購單)、GRN(收貨單/入庫單)、Invoice(供應商發票)之『品項、數量、未稅單價、稅額、總金額』是否一致。
                    2) 容差規則:預設數量差異容許 1%,金額差異容許 0.5%;超出即視為『例外』。
                    3) 稅率檢核:若發票稅率與 PO/合約稅率不一致,標記為例外。
                    4) 幣別與匯率:若幣別不同,請使用當日或指定結算日匯率進行換算後再比對。
                    5) 結果輸出:
                    - 若通過:建立『待付款』任務(含建議付款日與折扣期資訊)。
                    - 若例外:產生『例外事件單』並彙整差異原因、項目清單與建議處理人。
                    6) 僅可使用已註冊的工具進行查詢、比對與建立任務;不得捏造資料。
                    7) 若資訊不足,請引導輸入必要識別資訊(如 PO 編號、發票號碼、供應商代碼、入庫單號等)。
                    8) 請以清楚、條列化的商務語氣回覆;輸出包含『結論、依據、下一步』。
                """,
    Kernel = kernel,
    Arguments = new(new PromptExecutionSettings
    {
        // 允許自動挑選工具
        FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
    })
};
  • 建立對話 Thread
var agentThread = new ChatHistoryAgentThread();
  • 與 Agent 互動
 Console.Write("User > ");
string? userInput;
while ((userInput = Console.ReadLine()) is not null)
{
    if (string.IsNullOrWhiteSpace(userInput) ||
        userInput.Equals("exit", StringComparison.OrdinalIgnoreCase))
        break;

    var message = new ChatMessageContent(AuthorRole.User, userInput);

    bool isFirst = false;
    await foreach (StreamingChatMessageContent response in agent.InvokeStreamingAsync(message, agentThread))
    {
        if (!isFirst)
        {
            Console.Write($"{response.Role} - {response.AuthorName ?? "*"} > ");
            isFirst = true;
        }

        Console.Write($"{response.Content}");
    }

    Console.WriteLine();
    Console.WriteLine($"\n# trace chat thread with agent: {agent.Name} - {agent.Description}, threadId: {agentThread.Id}\n");
    Console.Write("User > ");
}

執行 AuditAgent

輸入User prompt 來啟動對帳稽核流程,例如:「幫我對帳:PO=PO-202509-001,GRN=GRN-7788,Invoice=INV-5566」,Agent 會依序呼叫:GetPurchaseOrderAsync → GetGoodsReceiptAsync → GetInvoiceAsync → MatchThreeWayAsync,最後根據對帳結果決定呼叫 CreatePaymentTaskAsync(通過) 或 CreateExceptionTicketAsync(不通過)。

=== Semantic Kernel AI Agent 系統 ===
User > 幫我對帳:PO=PO-202509-001,GRN=GRN-7788,Invoice=INV-5566
Assistant - AuditAgent > 結論:
- 三方對帳結果:PO(PO-202509-001)、GRN(GRN-7788)、Invoice(INV-5566)數量、金額等關鍵資訊皆在容差範圍內,對帳一致,無例外情形。

依據:
- 採購單、收貨單、發票各項品項、數量、單價、稅額及總金額核對均符合預設容差規則(數量1%、金額0.5%)。
- 未檢出稅率與幣別問題。

下一步:
- 請提供「建議付款日」(yyyy-MM-dd),以便自動建立待付款任務並依折扣期或付款條件排程。如需查詢折扣資訊,請告知。

# trace chat thread with agent: AuditAgent - 自動執行 PO/收貨單/發票 的三方對帳與例外處理的 AI 稽核助理, threadId: 45dd83fa884440dd8f473f3730b68d34

User > 2025/10/1 付款
Assistant - AuditAgent > 結論:
- 已成功為本次三方對帳結果(PO-202509-001 / INV-5566)建立「待付款」任務,建議付款日為 2025/10/1。

依據:
- 對帳無異常,所有關鍵數值皆符合公司容差及合約規範。
- 付款排程依據您的指定建議付款日(2025/10/1)。

下一步:
- 財務部門請依「待付款」任務編號(PAY-20250924121439)進行後續付款作業。
- 若有新的付款條件、提前折扣規劃或後續異動,歡迎隨時告知協助更新。

# trace chat thread with agent: AuditAgent - 自動執行 PO/收貨單/發票 的三方對帳與例外處理的 AI 稽核助理, threadId: 45dd83fa884440dd8f473f3730b68d34

結語

透過這個範例,我們展示了如何使用 Semantic Kernel Agent Framwork 建立 Single Agent ,並且結合自訂的 Plugin 來實作一個能夠自動化處理應付帳款三方對帳的 AI Agent,在這個過程中,利用了 Agent 的 Function Choice Behavior 來讓 Agent 根據使用者的需求自動選擇並呼叫適當的工具,實現了高度的靈活性與自動化決策。可以明顯感受到以 Agent 角度為設計與過去單純 Function 呼叫的差異,Agent 更像是一個有思考能力的助理,能夠根據情境做出適當的回應與行動。


上一篇
Day 9: 什麼!!多了一個 Semantic Kernel Agent Framework
下一篇
Day 11: Semantic Kernel Single Agent 實戰 - 銀行匯款 Agent
系列文
當 .NET 遇見 AI Agents:用 Semantic Kernel × MCP 打造智慧協作應用11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言