C# 后端集成 CodeBuddy CLI 实战指南
在现代 AI 代码助手开发中,单一 AI Provider 往往无法满足复杂多变的开发场景。这就像,人生路远,总不能只认一个方向吧?HagiCode 作为一款多功能 AI 编程助手,需要支持多种 AI Provider 以提供更好的用户体验。毕竟,用户的选择权还是要给够的。在 2026 年初,项目面临一个关键决策:如何在 C# 后端中恢复 CodeBuddy 的 ACP(Agent Communication Protocol)集成能力。 此前项目中曾实现过 CodeBuddy 对接,但相关代码在一次重构中被移除了。其实也没什么好抱怨的,代码迭代嘛,总有东西要被遗忘。本次技术方案的目标是完整恢复这一能力,并优化架构使其更加健壮和可维护。 如果你也在考虑为自己的项目接入多种 AI 编程助手,下面的方案或许能给你一些启发——这可是我们踩了无数坑之后总结出来的经验。或许能让你少走点弯路,也算是我做过的一点好事吧。 本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个开源的 AI 代码助手项目,支持多种 AI Provider 和跨平台运行。为了满足不同用户的偏好,我们需要能够灵活切换各种 AI 编程助手,这就有了本文要介绍的 CodeBuddy 集成方案。 HagiCode 采用模块化设计,AI Provider 作为可插拔的组件,这种架构让我们可以轻松添加新的 AI 支持,而不影响现有功能。这也罢了,设计这种东西,当初做得好,后面省心不少。如果你对我们的技术架构感兴趣,可以在 GitHub 上查看完整源码。 C# 与 CodeBuddy 的对接采用清晰的分层架构,这种设计让代码职责分明,后期维护起来也更加方便: 这种分层的好处是什么呢?简单说就是各层之间互不打扰。假设以后要换一种通信方式(比如从 stdio 改成 WebSocket),你只需要改最下面那一层,上面的业务代码完全不用动。毕竟,谁也不想牵一发而动全身,改个通信方式还要改半天业务代码,那也太惨了。 Provider 契约层 是整个架构的基石。我们定义了 Provider 工厂层 负责根据配置创建对应的 Provider 实例。这里使用了 .NET 的依赖注入机制,配合 Provider 实现层 是真正干活的地方。 ACP 基础设施层 则是通信的底层支撑。这一层处理所有的协议细节,包括进程管理、消息序列化、响应解析等。就像房子的地基,上面盖得再漂亮,底下的东西得稳才行。 CodeBuddy 使用 Stdio(标准输入输出) 方式与外部进程通信。启动命令很简单: 然后通过标准输入输出进行 JSON-RPC 消息交换。这种方式的优势在于: 通信过程中支持环境变量注入,常用的包括: 这就像,人与人之间的沟通,找个方便的方式,才能说得上话。 ACP 基于 JSON-RPC 2.0 协议,消息格式大概是酱紫的: 实际实现中,我们把这些协议细节都封装好了,上层业务代码只需要关注 prompt 和 response 就行。这也罢了,封装得好,后面的人用起来就舒服点。 首先在枚举文件中恢复 CodeBuddy 类型: 然后在扩展方法中添加字符串映射,这样配置文件就可以用字符串指定 Provider: 在工厂类中添加 CodeBuddy 的创建分支: 这里用了依赖注入的 下面是 这段代码有几个关键点: 在模块注册中添加相关服务: 在 对应的配置模型定义: 我们在实现过程中遇到了几个典型的坑,分享出来让大家少走弯路。毕竟,别人的坑,自己能避开就是好事: 性能这种事,能优化就优化,毕竟用户等的越久,体验就越差。 本文详细介绍了 C# 后端集成 CodeBuddy CLI 的完整方案,涵盖了从架构设计到具体实现的全过程。通过分层架构设计,我们将协议细节与业务逻辑分离,使得代码更加清晰和可维护。 核心要点回顾: 这套方案不仅适用于 CodeBuddy,添加新的 AI Provider 也遵循同样的模式。如果你也在做类似的多 AI Provider 集成,希望这篇文章能给你一些参考。其实,写文章和写代码一样,分享出来,能帮到别人就算没白写。 如果本文对你有帮助:C# 后端集成 CodeBuddy CLI 实战指南
本文将详细介绍如何在 C# 后端项目中集成 CodeBuddy CLI,实现 AI 编程助手能力的完整方案。
背景
关于 HagiCode
架构设计
分层架构概览
┌─────────────────────────────────────────────┐
│ Provider 契约层 │
│ AIProviderType 枚举 + 扩展方法 │
├─────────────────────────────────────────────┤
│ Provider 工厂层 │
│ AIProviderFactory 依赖注入工厂 │
├─────────────────────────────────────────────┤
│ Provider 实现层 │
│ CodebuddyCliProvider 具体实现 │
├─────────────────────────────────────────────┤
│ ACP 基础设施层 │
│ ACPSessionManager / StdioAcpTransport │
│ AcpRpcClient / AcpAgentClient │
└─────────────────────────────────────────────┘核心组件解析
AIProviderType 枚举,其中 CodebuddyCli = 3 作为枚举值,通过扩展方法实现字符串与枚举的双向映射。这样配置文件中的字符串可以很方便地转成枚举,调试时枚举也能转成字符串输出。这也罢了,其实就是个映射关系,但做好了就是省心。ActivatorUtilities.CreateInstance 实现动态创建。工厂模式的好处在于,新增一个 Provider 时只需要添加创建逻辑,不用修改已有的代码。这和写文章差不多,想加个新章节,就加个新章节,不用把前面的都重写一遍。CodebuddyCliProvider 实现了 IAIProvider 接口,提供 ExecuteAsync(非流式)和 StreamAsync(流式)两种调用方式。通信机制
Stdio 传输模式
codebuddy --acpCODEBUDDY_API_KEY:API 密钥认证CODEBUDDY_INTERNET_ENVIRONMENT:网络环境配置消息协议
// 请求消息
{
"jsonrpc": "2.0",
"id": 1,
"method": "agent/prompt",
"params": {
"prompt": "帮我写一个排序算法",
"sessionId": "session-123"
}
}
// 响应消息
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": "这里是 AI 的回复..."
}
}核心实现
1. Provider 契约恢复
// PCode.Models/AIProviderType.cs
public enum AIProviderType
{
ClaudeCodeCli = 0,
CodexCli = 1,
GitHubCopilot = 2,
CodebuddyCli = 3, // 恢复这个枚举值
OpenCodeCli = 4,
IFlowCli = 5,
}// AIProviderTypeExtensions.cs
private static readonly Dictionary<string, AIProviderType> _typeMap = new(
StringComparer.OrdinalIgnoreCase)
{
["CodebuddyCli"] = AIProviderType.CodebuddyCli,
["Codebuddy"] = AIProviderType.CodebuddyCli,
["codebuddy"] = AIProviderType.CodebuddyCli,
// ... 其他 provider 的映射
};2. Provider 工厂集成
// AIProviderFactory.cs
private IAIProvider? CreateProvider(AIProviderType providerType, ProviderConfiguration config)
{
return providerType switch
{
AIProviderType.CodebuddyCli =>
ActivatorUtilities.CreateInstance<CodebuddyCliProvider>(
_serviceProvider,
Options.Create(config)),
// ... 其他 provider
_ => throw new NotSupportedException($"Provider {providerType} not supported")
};
}ActivatorUtilities,它会自动处理构造函数的参数注入,非常方便。这也罢了,.NET 的东西,用对了就是省心。3. 完整的 Provider 实现
CodebuddyCliProvider 的核心实现,包含了流式和非流式两种调用方式:public class CodebuddyCliProvider : IAIProvider
{
private readonly ILogger<CodebuddyCliProvider> _logger;
private readonly IACPSessionManager _sessionManager;
private readonly ProviderConfiguration _config;
public string Name => "CodebuddyCli";
public bool SupportsStreaming => true;
public ProviderCapabilities Capabilities { get; }
public CodebuddyCliProvider(
ILogger<CodebuddyCliProvider> logger,
IACPSessionManager sessionManager,
IOptions<ProviderConfiguration> config)
{
_logger = logger;
_sessionManager = sessionManager;
_config = config.Value;
// 定义当前 Provider 的能力
Capabilities = new ProviderCapabilities
{
SupportsStreaming = true,
SupportsTools = true,
SupportsSystemMessages = true,
SupportsArtifacts = false,
MaxTokens = 8192
};
}
// 非流式调用:等所有结果一起返回
public async Task<AIResponse> ExecuteAsync(
AIRequest request,
CancellationToken cancellationToken = default)
{
// 为请求创建独立会话
var session = await _sessionManager.CreateSessionAsync(
"CodebuddyCli",
request.WorkingDirectory,
cancellationToken,
request.SessionId);
try
{
var fullPrompt = BuildPrompt(request);
await session.SendPromptAsync(fullPrompt, cancellationToken);
var responseBuilder = new StringBuilder();
var toolCalls = new List<AIToolCall>();
// 收集所有响应块
await foreach (var chunk in StreamFromSession(session, cancellationToken))
{
if (!string.IsNullOrEmpty(chunk.Content))
{
responseBuilder.Append(chunk.Content);
}
// 处理工具调用...
}
return new AIResponse
{
Content = AIResultContentSanitizer.SanitizeResultContent(
responseBuilder.ToString()),
ToolCalls = toolCalls,
Provider = Name,
Model = string.Empty
};
}
finally
{
// 释放会话资源
await session.DisposeAsync();
}
}
// 流式调用:实时返回响应块
public async IAsyncEnumerable<AIStreamingChunk> StreamAsync(
AIRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var session = await _sessionManager.CreateSessionAsync(
"CodebuddyCli",
request.WorkingDirectory,
cancellationToken);
try
{
var fullPrompt = BuildPrompt(request);
await session.SendPromptAsync(fullPrompt, cancellationToken);
await foreach (var chunk in StreamFromSession(session, cancellationToken))
{
yield return chunk;
}
}
finally
{
await session.DisposeAsync();
}
}
private async IAsyncEnumerable<AIStreamingChunk> StreamFromSession(
IACPSession session,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
// 遍历会话中的所有更新
await foreach (var notification in session.ReceiveUpdatesAsync(cancellationToken))
{
switch (notification.Update)
{
case AgentMessageChunkSessionUpdate agentMessage:
// 处理文本内容块
if (agentMessage.Content is AcpImp.TextContentBlock textContent)
{
yield return new AIStreamingChunk
{
Content = textContent.Text,
Type = StreamingChunkType.ContentDelta,
IsComplete = false
};
}
break;
case ToolCallSessionUpdate toolCall:
// 处理工具调用
yield return new AIStreamingChunk
{
Content = string.Empty,
Type = StreamingChunkType.ToolCallDelta,
ToolCallDelta = new AIToolCallDelta
{
Id = toolCall.ToolCallId,
Name = toolCall.Kind.ToString(),
Arguments = toolCall.RawInput?.ToString()
}
};
break;
case AcpImp.PromptCompletedSessionUpdate:
// 响应完成
yield break;
}
}
}
// 构建完整的提示词
private string BuildPrompt(AIRequest request, string? embeddedCommandPrompt = null)
{
var sb = new StringBuilder();
// 嵌入命令提示词(如果有)
if (!string.IsNullOrEmpty(embeddedCommandPrompt))
{
sb.AppendLine(embeddedCommandPrompt);
sb.AppendLine();
}
// 系统消息
if (!string.IsNullOrEmpty(request.SystemMessage))
{
sb.AppendLine(request.SystemMessage);
sb.AppendLine();
}
// 用户 prompt
sb.Append(request.Prompt);
return sb.ToString();
}
}IAsyncEnumerable 让响应可以边生成边返回,不用等全部内容生成完。这对于长文本场景特别重要,用户体验会好很多。就像,等结果的人也不想一直干等着不是。ToolCallSessionUpdate 处理。这个能力对于复杂的代码编辑任务很关键。AIResultContentSanitizer 过滤 Think 块内容,保持输出干净。4. 依赖注入配置
// PCodeClaudeHelperModule.cs
public void ConfigureModule(IServiceCollection context)
{
// 注册 Provider
context.Services.AddTransient<CodebuddyCliProvider>();
// 注册 ACP 基础设施
context.Services.AddSingleton<IACPSessionManager, ACPSessionManager>();
context.Services.AddSingleton<IAcpPlatformConfigurationResolver, AcpPlatformConfigurationResolver>();
context.Services.AddSingleton<IAIRequestToAcpMapper, AIRequestToAcpMapper>();
context.Services.AddSingleton<IAcpToAIResponseMapper, AcpToAIResponseMapper>();
}配置示例
配置文件
appsettings.json 中添加 CodeBuddy 相关配置:AI:
# 默认使用的 Provider
DefaultProvider: "CodebuddyCli"
# Provider 配置
Providers:
CodebuddyCli:
Type: "CodebuddyCli"
WorkingDirectory: "C:/projects/my-app"
ExecutablePath: "C:/tools/codebuddy.cmd"
# 平台相关配置
PlatformConfigurations:
CodebuddyCli:
ExecutablePath: "C:/tools/codebuddy.cmd"
Arguments: "--acp"
StartupTimeoutMs: 5000
EnvironmentVariables:
CODEBUDDY_API_KEY: "${CODEBUDDY_API_KEY}"
CODEBUDDY_INTERNET_ENVIRONMENT: "production"配置模型
public class CodebuddyPlatformConfiguration : IAcpPlatformConfiguration
{
public string ProviderName => "CodebuddyCli";
public AcpTransportType TransportType => AcpTransportType.Stdio;
public string ExecutablePath { get; set; } = "codebuddy";
public string Arguments { get; set; } = "--acp";
public int StartupTimeoutMs { get; set; } = 5000;
public Dictionary<string, string?>? EnvironmentVariables { get; set; }
}实践经验总结
踩坑记录
try-finally 确保每次请求都会释放资源。这也罢了,用过的东西得放回去,不然后面的人用什么。Dictionary<string, string?> 来处理。跨平台这种事,一开始就统一规范,后面就省心。性能优化
总结
参考资料