C# 开发者的 AI 新玩法,用 Semantic Kernel 对接 A2A 协议

从 DLL 到 A2A:C# CAD 插件的智能化转身

对于深耕 CAD 二次开发的 .NET 工程师而言,过去十年的工作流往往围绕着 DLL 加载、COM 互操作以及复杂的内存管理打转。我们习惯了在 C# 中直接调用几何内核的 API,通过指针操作三角形网格,或者在渲染循环中插入自定义的逻辑。然而,随着 AI Agent 生态的爆发式增长,传统的"本地库调用"模式正面临挑战:当你的 CAD 插件需要被一个运行在 Python 环境中的大模型调度器调用,或者需要与另一个负责供应链管理的 Java 智能体协作时,单纯的 DLL 暴露显得力不从心。

这就引出了本文的核心议题:如何利用微软的 Semantic Kernel (SK) 框架,将原本封闭在 C# 进程内的 CAD 几何分析能力,封装为符合 A2A (Agent-to-Agent) 协议的标准化智能体服务。这不仅仅是换个调用方式,更是让传统工业软件融入分布式 AI 生态的关键一步。我们将以一个具体的"薄壁检测"功能为例,展示如何让它从一个本地函数,变身成为网络上可被发现、可被调用的远程 Agent。

为什么 CAD 插件需要 A2A 协议?

在传统的 CAD 二次开发场景中,假设你编写了一个高性能的 C# 插件,能够利用 BVH(层次包围盒)加速算法,在毫秒级内找出 STL 模型中所有厚度小于 0.1mm 的薄壁区域。在单机环境下,这完美无缺:宿主程序加载你的 DLL,调用 GetThinParts() 方法,拿到结果列表。

但在多智能体协作的场景下,这种耦合度极高的方式就成了瓶颈。想象一个跨语言的自动化流程:一个基于 Python 的项目管理 Agent 接收到用户指令"检查这个零件的可制造性",它需要调用你的几何分析能力。如果依赖传统方式,Python 端必须通过复杂的 IPC(进程间通信)、REST API 包装甚至 C++/CLI 混合编译来桥接你的 C# 代码。这不仅增加了部署复杂度,还破坏了智能体之间的松耦合原则。

A2A 协议的出现正是为了解决这个问题。作为谷歌主导的开放标准,A2A 定义了智能体之间如何发现彼此(通过 Agent Card)、如何协商任务(Task)、以及如何交换结果(Artifact)。它不关心你的内部是用 C# 写的还是 Python 写的,只关心你是否遵循了标准的通信契约。

Semantic Kernel 在这里扮演了"翻译官"和"适配器"的角色。SK 原生支持将 C# 方法注册为 Plugin,并能轻松对接各种通信协议。通过 SK,我们可以将 CAD 内核的底层几何 API 包装成标准的 SK Plugin,再进一步通过 A2A Server 暴露出去。这样,外部的 Python 调度器只需发送一个标准的 JSON-RPC 请求,就能驱动本地的 C# 几何引擎工作,实现了真正的跨语言、跨平台协作。

核心架构:用 Semantic Kernel 桥接几何内核

要实现这一目标,我们需要构建一个分层架构。底层是现有的 CAD 几何内核(可能是自研的 C++ 库或通过 P/Invoke 调用的原生代码),中间层是 Semantic Kernel 定义的 Plugin,最上层则是符合 A2A 规范的 HTTP 服务端点。

1. 定义几何能力的 C# 接口

首先,我们需要确保底层的几何分析逻辑是清晰且无状态的。假设我们已经有一个经过优化的 GeometryEngine 类,它负责加载模型并执行薄壁检测。为了适配 SK,我们需要将其方法封装为 KernelFunction

csharp 复制代码
using Microsoft.SemanticKernel;
using System.ComponentModel;

public class CadGeometryPlugin
{
    private readonly GeometryEngine _engine;

    public CadGeometryPlugin()
    {
        // 初始化几何引擎,可能涉及内存池或 BVH 结构的预分配
        _engine = new GeometryEngine();
    }

    [KernelFunction("detect_thin_walls")]
    [Description("分析加载的 STL 模型,识别厚度小于指定阈值的薄壁区域。")]
    [return: Description("返回包含薄壁三角形 ID 列表及平均厚度的 JSON 字符串。")]
    public string DetectThinWalls(
        [Description("STL 文件的本地路径或已加载模型的 ID")] string modelSource, 
        [Description("厚度阈值,单位毫米,例如 0.1")] float maxThicknessMm)
    {
        // 调用底层高性能几何算法
        // 这里假设 LoadModel 和 GetThinParts 是内部优化过的零拷贝实现
        var modelId = _engine.LoadModel(modelSource);
        var thinParts = _engine.GetThinParts(modelId, maxThicknessMm);
        
        // 序列化为标准 JSON 格式,便于 A2A 协议传输
        return System.Text.Json.JsonSerializer.Serialize(new 
        {
            ModelId = modelId,
            Threshold = maxThicknessMm,
            Count = thinParts.Count,
            TriangleIds = thinParts.Select(t => t.Id).ToList(),
            AvgThickness = thinParts.Average(t => t.Thickness)
        });
    }
    
    [KernelFunction("get_model_bounds")]
    [Description("获取模型的包围盒信息,用于快速预判尺寸。")]
    public string GetModelBounds([Description("模型源")] string modelSource)
    {
        var bounds = _engine.GetBounds(modelSource);
        return System.Text.Json.JsonSerializer.Serialize(bounds);
    }
}

这段代码的关键在于 [KernelFunction][Description] 属性。Semantic Kernel 会读取这些元数据,自动生成函数的描述信息。这些信息至关重要,因为它们将被提取并填入 A2A 协议中的 Agent Card,让远程的 Python Agent 知道这个 C# 服务能做什么,需要什么参数。

2. 构建 A2A 兼容的服务端

有了 Plugin,接下来需要创建一个 HTTP 服务来监听 A2A 请求。虽然 A2A 协议底层基于 JSON-RPC 和 SSE(Server-Sent Events),但利用 SK 的扩展性,我们可以手动构建一个轻量级的 ASP.NET Core 控制器来处理协议握手和任务分发。

A2A 协议的核心交互流程包括:能力发现 (GET /agent/card)、任务提交 (POST /tasks/send)和状态轮询/推送

csharp 复制代码
using Microsoft.AspNetCore.Mvc;
using Microsoft.SemanticKernel;
using System.Text.Json;

[ApiController]
[Route("api/a2a")]
public class A2AAgentController : ControllerBase
{
    private readonly Kernel _kernel;
    private readonly ILogger<A2AAgentController> _logger;

    // 模拟任务存储,实际生产环境应使用 Redis 或数据库
    private static readonly Dictionary<string, TaskState> _taskStore = new();

    public A2AAgentController(Kernel kernel, ILogger<A2AAgentController> logger)
    {
        _kernel = kernel;
        _logger = logger;
    }

    // 1. 能力发现:返回 Agent Card
    [HttpGet("card")]
    public IActionResult GetAgentCard()
    {
        var card = new
        {
            name = "CAD Geometry Analyst",
            description = "专业的 CAD 几何分析智能体,提供薄壁检测、干涉检查和包围盒计算服务。",
            url = "https://localhost:5001/api/a2a",
            version = "1.0.0",
            capabilities = new { streaming = true, pushNotifications = false },
            skills = new[]
            {
                new {
                    id = "detect_thin_walls",
                    name = "薄壁检测",
                    description = "识别模型中厚度低于阈值的区域,适用于 DFM 分析。",
                    inputModes = new[] { "application/json" },
                    outputModes = new[] { "application/json" }
                },
                new {
                    id = "get_model_bounds",
                    name = "包围盒计算",
                    description = "快速获取模型的三维空间范围。",
                    inputModes = new[] { "application/json" }
                }
            }
        };
        return Ok(card);
    }

    // 2. 任务提交:接收 Remote Agent 的请求
    [HttpPost("tasks/send")]
    public async Task<IActionResult> SendTask([FromBody] JsonElement request)
    {
        // 解析 A2A 标准请求结构
        var taskId = Guid.NewGuid().ToString();
        var method = request.GetProperty("method").GetString();
        var parameters = request.GetProperty("params");
        
        // 初始化任务状态
        _taskStore[taskId] = new TaskState { Status = "working", Messages = new List<string>() };

        try
        {
            // 映射 A2A 方法名到 SK Plugin 函数名
            // 这里需要根据实际协议字段做更严谨的映射逻辑
            var functionName = "detect_thin_walls"; 
            var args = parameters.GetProperty("message").GetProperty("parts")[0].GetProperty("text").GetString();
            
            // 反序列化参数以便 SK 调用
            var inputArgs = JsonSerializer.Deserialize<Dictionary<string, object>>(args);
            
            // 调用 Semantic Kernel 执行 C# 方法
            var result = await _kernel.InvokeAsync<CadGeometryPlugin>(
                functionName, 
                new KernelArguments(inputArgs)
            );

            // 更新任务状态为完成
            _taskStore[taskId].Status = "completed";
            _taskStore[taskId].Result = result.ToString();

            // 构造 A2A 标准响应
            var response = new
            {
                jsonrpc = "2.0",
                id = request.GetProperty("id"),
                result = new
                {
                    id = taskId,
                    status = new { state = "completed" },
                    artifacts = new[] 
                    { 
                        new { 
                            name = "analysis_result", 
                            parts = new[] { new { type = "text", text = result.ToString() } } 
                        } 
                    }
                }
            };
            return Ok(response);
        }
        catch (Exception ex)
        {
            _taskStore[taskId].Status = "failed";
            _logger.LogError(ex, "Task execution failed");
            return StatusCode(500, new { error = ex.Message });
        }
    }
    
    // 3. 任务状态查询 (简化版)
    [HttpGet("tasks/{taskId}/status")]
    public IActionResult GetTaskStatus(string taskId)
    {
        if (_taskStore.TryGetValue(taskId, out var state))
        {
            return Ok(new { id = taskId, status = new { state = state.Status }, result = state.Result });
        }
        return NotFound();
    }
}

public class TaskState
{
    public string Status { get; set; }
    public string Result { get; set; }
    public List<string> Messages { get; set; }
}

上述代码展示了如何将 C# 的业务逻辑嵌入到 A2A 的协议框架中。GetAgentCard 方法生成的 JSON 就是智能体的"身份证",任何遵循 A2A 协议的客户端(无论是 Python 脚本还是其他语言的 Agent 框架)都可以通过读取这个 URL 了解到该服务具备"薄壁检测"和"包围盒计算"的能力。而 SendTask 方法则负责接收标准化的任务请求,利用 Semantic Kernel 的动态调用能力执行具体的 C# 代码,并将结果封装回 A2A 格式的 Artifact

跨语言协作实战:Python 调度器调用 C# 几何服务

为了验证这套架构的有效性,我们可以模拟一个典型的跨语言场景:一个运行在 Python 环境中的主调度器(Client Agent),需要调用上述 C# 服务来分析一个零件。

在 Python 端,无需安装任何 .NET 运行时或复杂的桥接库,只需要一个标准的 HTTP 客户端(如 httpxrequests)即可。Python 脚本首先访问 C# 服务的 /card 接口,解析出可用的技能,然后构造符合 A2A 规范的 JSON-RPC 请求。

python 复制代码
import httpx
import json

async def invoke_cad_agent():
    agent_url = "http://localhost:5001/api/a2a"
    
    async with httpx.AsyncClient() as client:
        # 1. 发现能力
        card_resp = await client.get(f"{agent_url}/card")
        card = card_resp.json()
        print(f"发现智能体:{card['name']} - {card['description']}")
        
        # 2. 构造任务请求 (模拟 A2A 协议格式)
        task_payload = {
            "jsonrpc": "2.0",
            "id": 1,
            "method": "tasks/send",
            "params": {
                "id": "task-001",
                "message": {
                    "role": "user",
                    "parts": [{
                        "type": "text",
                        "text": json.dumps({
                            "modelSource": "C:/models/engine_block.stl",
                            "maxThicknessMm": 0.15
                        })
                    }]
                }
            }
        }
        
        # 3. 发送请求并获取结果
        print("正在发送薄壁检测任务...")
        response = await client.post(f"{agent_url}/tasks/send", json=task_payload)
        result = response.json()
        
        if 'result' in result:
            artifact = result['result']['artifacts'][0]['parts'][0]['text']
            analysis_data = json.loads(artifact)
            print(f"检测完成!发现 {analysis_data['Count']} 个薄壁区域。")
            print(f"平均厚度:{analysis_data['AvgThickness']:.4f} mm")
        else:
            print("任务执行失败")

# 运行异步入口
# asyncio.run(invoke_cad_agent())

在这个流程中,Python 端完全不需要知道 C# 内部是如何利用 BVH 加速射线相交检测的,也不需要关心内存池是如何管理的。它只关心输入(文件路径、阈值)和输出(JSON 结果)。这种解耦极大地降低了系统集成的难度。如果未来需要将 C# 服务迁移到 Docker 容器中,或者替换为 C++ 重写的高性能版本,只要保持 A2A 接口不变,Python 调度器无需修改任何代码。

调试技巧与工程化落地建议

将传统的 CAD 插件改造为 A2A 智能体,除了代码层面的转换,还需要注意以下几个工程化细节,以确保服务的稳定性和可维护性。

首先是状态管理与超时处理 。CAD 几何计算(尤其是针对百万级三角面片的模型)可能耗时较长。在 A2A 协议中,推荐使用异步任务模式。上述示例为了简洁展示了同步返回,但在生产环境中,SendTask 应立即返回 submittedworking 状态,并通过 SSE(Server-Sent Events)或客户端轮询机制通知任务进度。Semantic Kernel 支持异步函数执行,结合 ASP.NET Core 的后台服务(BackgroundService),可以轻松实现长任务的队列化处理,避免 HTTP 请求超时。

其次是安全性与鉴权 。暴露几何分析能力意味着可能涉及敏感的设计图纸数据。A2A 协议支持多种认证机制,如 Bearer Token 或 OAuth2。在 ASP.NET Core 中间件中,应添加相应的认证策略,确保只有持有有效凭证的 Remote Agent 才能调用 detect_thin_walls 等敏感方法。此外,建议在 Plugin 层增加参数校验逻辑,防止恶意的大文件或非法路径输入导致服务崩溃。

最后是日志与可观察性。在分布式系统中,追踪一个请求从 Python 发起,经过网络传输,进入 C# 内核,再返回结果的全链路至关重要。利用 Semantic Kernel 内置的遥测支持(Telemetry),可以将函数调用的耗时、输入输出摘要等信息发送到 OpenTelemetry 兼容的后端。这对于排查"为什么这次薄壁检测比上次慢了 200ms"这类性能问题非常有帮助。

结语

通过 Semantic Kernel 和 A2A 协议,C# 开发者不再局限于单机版的 DLL 插件开发模式。我们将厚重的几何内核能力轻量化、服务化,使其成为多智能体生态中一个标准的、可被任意语言调用的节点。这不仅延长了传统 CAD 二次开发代码的生命周期,更为工业软件接入未来的自主 AI 系统打开了一扇大门。当下一次你需要让 CAD"听懂"自然语言指令,或者与其他系统的智能体协同工作时,这套架构将是你最坚实的后盾。

相关推荐
许泽宇的技术分享6 个月前
解锁AI工作流的终极密码:Semantic Kernel Process框架深度技术解析
semantic kernel
许泽宇的技术分享9 个月前
Semantic Kernel Agent:微软打造的AI智能体开发“神器“——从零到一玩转企业级AI助手
人工智能·microsoft·semantic kernel
菜鸟分享录1 年前
将MCP(ModelContextProtocol)与Semantic Kernel集成(调用github)
ai·.netcore·semantic kernel·mcp
菜鸟分享录1 年前
MCP 入门实战:用 C# 开启 AI 新篇章
ai·c#·semantic kernel·mcp
菜鸟分享录1 年前
使用 Semantic Kernel 快速对接国产大模型实战指南(DeepSeek/Qwen/GLM)
microsoft·.netcore·semantic kernel
码观天工1 年前
.NET 原生驾驭 AI 新基建实战系列(二):Semantic Kernel 整合对向量数据库的统一支持
ai·.net·向量数据库·semantic kernel
码观天工1 年前
.NET原生操作向量数据库实战系列(一):.向量数据库的应用与AI时代下的畅想
c#·.net·semantic kernel·ml.net
yi念之间2 年前
C#整合Ollama实现本地LLMs调用
大模型·semantic kernel
绿荫阿广2 年前
使用Aspire优雅的进行全栈开发——WinUI使用Semantic Kernel调用智普清言LLM总结Asp.Net Core通过Playwright解析的网页内容
asp.net core·winui·semantic kernel