C#后端集成CodeBuddy CLI的完整方案
本文將詳細(xì)介紹如何在 C# 后端項(xiàng)目中集成 CodeBuddy CLI,實(shí)現(xiàn) AI 編程助手能力的完整方案。
背景
在現(xiàn)代 AI 代碼助手開發(fā)中,單一 AI Provider 往往無法滿足復(fù)雜多變的開發(fā)場景。這就像,人生路遠(yuǎn),總不能只認(rèn)一個(gè)方向吧?HagiCode 作為一款多功能 AI 編程助手,需要支持多種 AI Provider 以提供更好的用戶體驗(yàn)。畢竟,用戶的選擇權(quán)還是要給夠的。在 2026 年初,項(xiàng)目面臨一個(gè)關(guān)鍵決策:如何在 C# 后端中恢復(fù) CodeBuddy 的 ACP(Agent Communication Protocol)集成能力。
此前項(xiàng)目中曾實(shí)現(xiàn)過 CodeBuddy 對接,但相關(guān)代碼在一次重構(gòu)中被移除了。其實(shí)也沒什么好抱怨的,代碼迭代嘛,總有東西要被遺忘。本次技術(shù)方案的目標(biāo)是完整恢復(fù)這一能力,并優(yōu)化架構(gòu)使其更加健壯和可維護(hù)。
如果你也在考慮為自己的項(xiàng)目接入多種 AI 編程助手,下面的方案或許能給你一些啟發(fā)——這可是我們踩了無數(shù)坑之后總結(jié)出來的經(jīng)驗(yàn)。或許能讓你少走點(diǎn)彎路,也算是我做過的一點(diǎn)好事吧。
關(guān)于 HagiCode
本文分享的方案來自我們在 HagiCode 項(xiàng)目中的實(shí)踐經(jīng)驗(yàn)。HagiCode 是一個(gè)開源的 AI 代碼助手項(xiàng)目,支持多種 AI Provider 和跨平臺運(yùn)行。為了滿足不同用戶的偏好,我們需要能夠靈活切換各種 AI 編程助手,這就有了本文要介紹的 CodeBuddy 集成方案。
HagiCode 采用模塊化設(shè)計(jì),AI Provider 作為可插拔的組件,這種架構(gòu)讓我們可以輕松添加新的 AI 支持,而不影響現(xiàn)有功能。這也罷了,設(shè)計(jì)這種東西,當(dāng)初做得好,后面省心不少。如果你對我們的技術(shù)架構(gòu)感興趣,可以在 GitHub 上查看完整源碼。
架構(gòu)設(shè)計(jì)
分層架構(gòu)概覽
C# 與 CodeBuddy 的對接采用清晰的分層架構(gòu),這種設(shè)計(jì)讓代碼職責(zé)分明,后期維護(hù)起來也更加方便:
┌─────────────────────────────────────────────┐ │ Provider 契約層 │ │ AIProviderType 枚舉 + 擴(kuò)展方法 │ ├─────────────────────────────────────────────┤ │ Provider 工廠層 │ │ AIProviderFactory 依賴注入工廠 │ ├─────────────────────────────────────────────┤ │ Provider 實(shí)現(xiàn)層 │ │ CodebuddyCliProvider 具體實(shí)現(xiàn) │ ├─────────────────────────────────────────────┤ │ ACP 基礎(chǔ)設(shè)施層 │ │ ACPSessionManager / StdioAcpTransport │ │ AcpRpcClient / AcpAgentClient │ └─────────────────────────────────────────────┘
這種分層的好處是什么呢?簡單說就是各層之間互不打擾。假設(shè)以后要換一種通信方式(比如從 stdio 改成 WebSocket),你只需要改最下面那一層,上面的業(yè)務(wù)代碼完全不用動。畢竟,誰也不想牽一發(fā)而動全身,改個(gè)通信方式還要改半天業(yè)務(wù)代碼,那也太慘了。
核心組件解析
Provider 契約層 是整個(gè)架構(gòu)的基石。我們定義了 AIProviderType 枚舉,其中 CodebuddyCli = 3 作為枚舉值,通過擴(kuò)展方法實(shí)現(xiàn)字符串與枚舉的雙向映射。這樣配置文件中的字符串可以很方便地轉(zhuǎn)成枚舉,調(diào)試時(shí)枚舉也能轉(zhuǎn)成字符串輸出。這也罷了,其實(shí)就是個(gè)映射關(guān)系,但做好了就是省心。
Provider 工廠層 負(fù)責(zé)根據(jù)配置創(chuàng)建對應(yīng)的 Provider 實(shí)例。這里使用了 .NET 的依賴注入機(jī)制,配合 ActivatorUtilities.CreateInstance 實(shí)現(xiàn)動態(tài)創(chuàng)建。工廠模式的好處在于,新增一個(gè) Provider 時(shí)只需要添加創(chuàng)建邏輯,不用修改已有的代碼。這和寫文章差不多,想加個(gè)新章節(jié),就加個(gè)新章節(jié),不用把前面的都重寫一遍。
Provider 實(shí)現(xiàn)層 是真正干活的地方。CodebuddyCliProvider 實(shí)現(xiàn)了 IAIProvider 接口,提供 ExecuteAsync(非流式)和 StreamAsync(流式)兩種調(diào)用方式。
ACP 基礎(chǔ)設(shè)施層 則是通信的底層支撐。這一層處理所有的協(xié)議細(xì)節(jié),包括進(jìn)程管理、消息序列化、響應(yīng)解析等。就像房子的地基,上面蓋得再漂亮,底下的東西得穩(wěn)才行。
通信機(jī)制
Stdio 傳輸模式
CodeBuddy 使用 Stdio(標(biāo)準(zhǔn)輸入輸出) 方式與外部進(jìn)程通信。啟動命令很簡單:
codebuddy --acp
然后通過標(biāo)準(zhǔn)輸入輸出進(jìn)行 JSON-RPC 消息交換。這種方式的優(yōu)勢在于:
- 啟動迅速:本地進(jìn)程通信沒有網(wǎng)絡(luò)延遲
- 配置簡單:只需要指定可執(zhí)行文件路徑
- 環(huán)境隔離:每個(gè)會話獨(dú)立進(jìn)程,互不影響
通信過程中支持環(huán)境變量注入,常用的包括:
CODEBUDDY_API_KEY:API 密鑰認(rèn)證CODEBUDDY_INTERNET_ENVIRONMENT:網(wǎng)絡(luò)環(huán)境配置
這就像,人與人之間的溝通,找個(gè)方便的方式,才能說得上話。
消息協(xié)議
ACP 基于 JSON-RPC 2.0 協(xié)議,消息格式大概是醬紫的:
// 請求消息
{
"jsonrpc": "2.0",
"id": 1,
"method": "agent/prompt",
"params": {
"prompt": "幫我寫一個(gè)排序算法",
"sessionId": "session-123"
}
}
// 響應(yīng)消息
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": "這里是 AI 的回復(fù)..."
}
}實(shí)際實(shí)現(xiàn)中,我們把這些協(xié)議細(xì)節(jié)都封裝好了,上層業(yè)務(wù)代碼只需要關(guān)注 prompt 和 response 就行。這也罷了,封裝得好,后面的人用起來就舒服點(diǎn)。
核心實(shí)現(xiàn)
1. Provider 契約恢復(fù)
首先在枚舉文件中恢復(fù) CodeBuddy 類型:
// PCode.Models/AIProviderType.cs
public enum AIProviderType
{
ClaudeCodeCli = 0,
CodexCli = 1,
GitHubCopilot = 2,
CodebuddyCli = 3, // 恢復(fù)這個(gè)枚舉值
OpenCodeCli = 4,
IFlowCli = 5,
}然后在擴(kuò)展方法中添加字符串映射,這樣配置文件就可以用字符串指定 Provider:
// AIProviderTypeExtensions.cs
private static readonly Dictionary<string, AIProviderType> _typeMap = new(
StringComparer.OrdinalIgnoreCase)
{
["CodebuddyCli"] = AIProviderType.CodebuddyCli,
["Codebuddy"] = AIProviderType.CodebuddyCli,
["codebuddy"] = AIProviderType.CodebuddyCli,
// ... 其他 provider 的映射
};2. Provider 工廠集成
在工廠類中添加 CodeBuddy 的創(chuàng)建分支:
// 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,它會自動處理構(gòu)造函數(shù)的參數(shù)注入,非常方便。這也罷了,.NET 的東西,用對了就是省心。
3. 完整的 Provider 實(shí)現(xiàn)
下面是 CodebuddyCliProvider 的核心實(shí)現(xiàn),包含了流式和非流式兩種調(diào)用方式:
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;
// 定義當(dāng)前 Provider 的能力
Capabilities = new ProviderCapabilities
{
SupportsStreaming = true,
SupportsTools = true,
SupportsSystemMessages = true,
SupportsArtifacts = false,
MaxTokens = 8192
};
}
// 非流式調(diào)用:等所有結(jié)果一起返回
public async Task<AIResponse> ExecuteAsync(
AIRequest request,
CancellationToken cancellationToken = default)
{
// 為請求創(chuàng)建獨(dú)立會話
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>();
// 收集所有響應(yīng)塊
await foreach (var chunk in StreamFromSession(session, cancellationToken))
{
if (!string.IsNullOrEmpty(chunk.Content))
{
responseBuilder.Append(chunk.Content);
}
// 處理工具調(diào)用...
}
return new AIResponse
{
Content = AIResultContentSanitizer.SanitizeResultContent(
responseBuilder.ToString()),
ToolCalls = toolCalls,
Provider = Name,
Model = string.Empty
};
}
finally
{
// 釋放會話資源
await session.DisposeAsync();
}
}
// 流式調(diào)用:實(shí)時(shí)返回響應(yīng)塊
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:
// 處理文本內(nèi)容塊
if (agentMessage.Content is AcpImp.TextContentBlock textContent)
{
yield return new AIStreamingChunk
{
Content = textContent.Text,
Type = StreamingChunkType.ContentDelta,
IsComplete = false
};
}
break;
case ToolCallSessionUpdate toolCall:
// 處理工具調(diào)用
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:
// 響應(yīng)完成
yield break;
}
}
}
// 構(gòu)建完整的提示詞
private string BuildPrompt(AIRequest request, string? embeddedCommandPrompt = null)
{
var sb = new StringBuilder();
// 嵌入命令提示詞(如果有)
if (!string.IsNullOrEmpty(embeddedCommandPrompt))
{
sb.AppendLine(embeddedCommandPrompt);
sb.AppendLine();
}
// 系統(tǒng)消息
if (!string.IsNullOrEmpty(request.SystemMessage))
{
sb.AppendLine(request.SystemMessage);
sb.AppendLine();
}
// 用戶 prompt
sb.Append(request.Prompt);
return sb.ToString();
}
}
這段代碼有幾個(gè)關(guān)鍵點(diǎn):
- 會話管理:每個(gè)請求創(chuàng)建獨(dú)立會話,請求完成后釋放資源。這是坑踩出來的經(jīng)驗(yàn)——如果會話復(fù)用做得不好,很容易出現(xiàn)狀態(tài)污染的問題。畢竟,用過就得收拾干凈,不然下次用的人就麻煩了。
- 流式處理:
IAsyncEnumerable讓響應(yīng)可以邊生成邊返回,不用等全部內(nèi)容生成完。這對于長文本場景特別重要,用戶體驗(yàn)會好很多。就像,等結(jié)果的人也不想一直干等著不是。 - 工具調(diào)用:CodeBuddy 支持工具調(diào)用(Function Calling),通過
ToolCallSessionUpdate處理。這個(gè)能力對于復(fù)雜的代碼編輯任務(wù)很關(guān)鍵。 - 內(nèi)容過濾:使用
AIResultContentSanitizer過濾 Think 塊內(nèi)容,保持輸出干凈。
4. 依賴注入配置
在模塊注冊中添加相關(guān)服務(wù):
// PCodeClaudeHelperModule.cs
public void ConfigureModule(IServiceCollection context)
{
// 注冊 Provider
context.Services.AddTransient<CodebuddyCliProvider>();
// 注冊 ACP 基礎(chǔ)設(shè)施
context.Services.AddSingleton<IACPSessionManager, ACPSessionManager>();
context.Services.AddSingleton<IAcpPlatformConfigurationResolver, AcpPlatformConfigurationResolver>();
context.Services.AddSingleton<IAIRequestToAcpMapper, AIRequestToAcpMapper>();
context.Services.AddSingleton<IAcpToAIResponseMapper, AcpToAIResponseMapper>();
}
配置示例
配置文件
在 appsettings.json 中添加 CodeBuddy 相關(guān)配置:
AI:
# 默認(rèn)使用的 Provider
DefaultProvider: "CodebuddyCli"
# Provider 配置
Providers:
CodebuddyCli:
Type: "CodebuddyCli"
WorkingDirectory: "C:/projects/my-app"
ExecutablePath: "C:/tools/codebuddy.cmd"
# 平臺相關(guān)配置
PlatformConfigurations:
CodebuddyCli:
ExecutablePath: "C:/tools/codebuddy.cmd"
Arguments: "--acp"
StartupTimeoutMs: 5000
EnvironmentVariables:
CODEBUDDY_API_KEY: "${CODEBUDDY_API_KEY}"
CODEBUDDY_INTERNET_ENVIRONMENT: "production"配置模型
對應(yīng)的配置模型定義:
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; }
}實(shí)踐經(jīng)驗(yàn)總結(jié)
踩坑記錄
我們在實(shí)現(xiàn)過程中遇到了幾個(gè)典型的坑,分享出來讓大家少走彎路。畢竟,別人的坑,自己能避開就是好事:
- 會話泄漏問題:一開始沒有正確釋放會話,導(dǎo)致進(jìn)程資源耗盡。解決方法是使用
try-finally確保每次請求都會釋放資源。這也罷了,用過的東西得放回去,不然后面的人用什么。 - 環(huán)境變量傳遞:Windows 和 Linux 的環(huán)境變量語法不同,后來統(tǒng)一使用
Dictionary<string, string?>來處理。跨平臺這種事,一開始就統(tǒng)一規(guī)范,后面就省心。 - 超時(shí)配置:CLI 啟動需要時(shí)間,設(shè)置了 5 秒的啟動超時(shí),避免快速請求失敗。凡事都得有個(gè)度,太急了反而辦不成事。
- 編碼問題:Windows 上默認(rèn)編碼可能導(dǎo)致中文亂碼,在啟動進(jìn)程時(shí)顯式指定 UTF-8 編碼。中文顯示不出來,那多難受。
性能優(yōu)化
- 會話池:對于頻繁的短請求,可以考慮實(shí)現(xiàn)會話池來復(fù)用進(jìn)程
- 連接緩存:工廠類已經(jīng)支持 Provider 實(shí)例緩存
- 異步優(yōu)先:全程使用異步編程,避免阻塞線程
性能這種事,能優(yōu)化就優(yōu)化,畢竟用戶等的越久,體驗(yàn)就越差。
總結(jié)
本文詳細(xì)介紹了 C# 后端集成 CodeBuddy CLI 的完整方案,涵蓋了從架構(gòu)設(shè)計(jì)到具體實(shí)現(xiàn)的全過程。通過分層架構(gòu)設(shè)計(jì),我們將協(xié)議細(xì)節(jié)與業(yè)務(wù)邏輯分離,使得代碼更加清晰和可維護(hù)。
核心要點(diǎn)回顧:
- 采用 Provider 契約層、工廠層、實(shí)現(xiàn)層、基礎(chǔ)設(shè)施層的分層架構(gòu)
- 使用 JSON-RPC over Stdio 方式進(jìn)行進(jìn)程間通信
- 通過依賴注入實(shí)現(xiàn)靈活的配置和擴(kuò)展
- 提供流式和非流式兩種調(diào)用方式
這套方案不僅適用于 CodeBuddy,添加新的 AI Provider 也遵循同樣的模式。如果你也在做類似的多 AI Provider 集成,希望這篇文章能給你一些參考。其實(shí),寫文章和寫代碼一樣,分享出來,能幫到別人就算沒白寫。
以上就是C#后端集成CodeBuddy CLI的完整方案的詳細(xì)內(nèi)容,更多關(guān)于C#集成CodeBuddy CLI的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#微信公眾號開發(fā)之使用MessageHandler簡化消息處理流程
這篇文章介紹了C#微信公眾號開發(fā)之使用MessageHandler簡化消息處理流程,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06
C# DataTable與Model互轉(zhuǎn)的示例代碼
這篇文章主要介紹了C#DataTable與Model互轉(zhuǎn)的示例代碼,幫助大家更好的理解和使用c#,感興趣的朋友可以了解下2020-12-12
C#基于DBContext(EF)實(shí)現(xiàn)通用增刪改查的REST方法實(shí)例
這篇文章主要介紹了C#基于DBContext(EF)實(shí)現(xiàn)通用增刪改查的REST方法實(shí)例,是C#程序設(shè)計(jì)中非常實(shí)用的技巧,需要的朋友可以參考下2014-10-10
C#簡單實(shí)現(xiàn)表達(dá)式目錄樹(Expression)
表達(dá)式目錄樹以數(shù)據(jù)形式表示語言級別代碼。數(shù)據(jù)存儲在樹形結(jié)構(gòu)中。表達(dá)式目錄樹中的每個(gè)節(jié)點(diǎn)都表示一個(gè)表達(dá)式。這篇文章給大家介紹C#簡單實(shí)現(xiàn)表達(dá)式目錄樹(Expression),需要的朋友參考下吧2017-11-11
DevExpress實(shí)現(xiàn)GridView當(dāng)無數(shù)據(jù)行時(shí)提示消息
這篇文章主要介紹了DevExpress實(shí)現(xiàn)GridView當(dāng)無數(shù)據(jù)行時(shí)提示消息,需要的朋友可以參考下2014-08-08

