Skip to content

HagiCode

2 posts with the tag “HagiCode”

豆包语音识别热词功能实现指南

豆包语音识别热词功能实现指南

Section titled “豆包语音识别热词功能实现指南”

本文将详细介绍如何在 HagiCode 项目中实现豆包语音识别的热词支持功能,通过自定义热词和平台热词表两种方式,显著提升特定领域词汇的识别准确率。

语音识别技术发展这么多年了,其实有个问题一直困扰着开发者们。通用语音识别模型虽然能覆盖日常用语,可对于专业术语、产品名称、人名这些词,识别准确率总差那么点意思。想想看,医疗领域的语音助手要准确识别”高血压”、“糖尿病”、“冠心病”;法律系统要精准捕捉”案由”、“答辩”、“举证责任”——这些场景下,通用模型的表现怎么说呢,也算尽力了。

在 HagiCode 项目中,我们也遇到了同样的挑战。作为一个多功能的 AI 代码助手,HagiCode 需要处理各种技术术语的语音识别场景。然而,豆包语音识别 API 在默认情况下,并不能完全满足我们对专业术语准确率的那些要求。其实也不是豆包不够好,只是每个领域都有自己的一套术语体系。经过一番调研和技术探索,我们发现豆包语音识别 API 实际上提供了热词支持功能,只要简单配置一下,就能显著提升特定词汇的识别准确率。这倒是有点像,你告诉它你要注意什么词,它就会更用心去听那些词。

本文要分享的,就是在 HagiCode 项目中实现豆包语音识别热词功能的完整方案。两种模式,自定义热词和平台热词表,都可以用,也都能组合用。通过这套方案,开发者可以根据业务场景灵活配置热词,让语音识别系统”认识”那些专业、罕见但又至关重要的词汇。

本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个开源的 AI 代码助手项目,技术栈还算现代化,旨在为开发者提供智能化的编程辅助体验。作为一个多语言、多平台的复杂项目,HagiCode 需要处理各种技术术语的语音识别场景,这也推动了我们对热词功能的研究和实现。

如果你对 HagiCode 的技术实现感兴趣,可以访问 GitHub 仓库 了解更多信息,也可以查看我们的 官方文档 了解完整的安装和使用指南。

豆包语音识别 API 为我们提供了两种热词配置方式,每种方式都有其独特的应用场景和优势。

自定义热词模式允许我们通过 corpus.context 字段直接传递热词文本。这种方式非常适合需要快速配置少量热词的场景,比如临时需要识别某个产品名称或者人名。在 HagiCode 的实现中,我们将用户输入的多行热词文本解析为字符串列表,然后按照豆包 API 的要求格式化为 context_data 数组。怎么说呢,这种方式很直接,就像告诉对方”你要注意这些词”,然后它就去注意了。

平台热词表模式则通过 corpus.boosting_table_id 字段引用豆包自学习平台预配置的热词表。这种方式适合需要管理大量热词的场景,我们可以在豆包自学习平台上创建和维护热词表,然后通过 ID 进行引用。对于 HagiCode 这类需要持续更新和维护专业术语的项目来说,这种模式提供了更好的可管理性。毕竟,热词多了之后,找个地方统一管理,总比每次都要手动输入要好。

有意思的是,这两种模式还可以组合使用。豆包 API 支持在同一个请求中同时包含自定义热词和平台热词表 ID,通过 combine_mode 参数控制组合策略。这种灵活性使得 HagiCode 能够应对各种复杂的专业术语识别需求。这也倒是挺好,有时候多种方式组合一下,效果可能更好。

在 HagiCode 的前端实现中,我们定义了一套完整的热词配置类型和验证逻辑。首先是类型定义部分:

export interface HotwordConfig {
contextText: string; // 多行热词文本
boostingTableId: string; // 豆包平台热词表 ID
combineMode: boolean; // 是否组合使用
}

这个简单的接口包含了热词功能的所有配置项。其中 contextText 是用户最直观感受到的部分——我们允许用户每行输入一个热词短语,这种方式非常符合直觉。毕竟,让用户一行一个词,总比让用户理解复杂的配置规则要好。

接下来是验证函数的实现。考虑到豆包 API 的限制,我们制定了严格的验证规则:热词文本最多 100 行,每行最多 50 个字符,总共最多 5000 个字符;boosting_table_id 最多 200 个字符,只允许字母、数字、下划线和连字符。这些限制不是我们凭空想象的,而是基于豆包官方文档的实际要求。毕竟,API 的限制就是 API 的限制,我们也没办法,只能遵守。

export function validateContextText(contextText: string): HotwordValidationResult {
if (!contextText || contextText.trim().length === 0) {
return { isValid: true, errors: [] };
}
const lines = contextText.split('\n').filter(line => line.trim().length > 0);
const errors: string[] = [];
if (lines.length > 100) {
errors.push(`热词行数不能超过 100 行,当前为 ${lines.length}`);
}
const totalChars = contextText.length;
if (totalChars > 5000) {
errors.push(`热词总字符数不能超过 5000,当前为 ${totalChars}`);
}
for (let i = 0; i < lines.length; i++) {
if (lines[i].length > 50) {
errors.push(`${i + 1} 行热词超过 50 个字符限制`);
}
}
return { isValid: errors.length === 0, errors };
}
export function validateBoostingTableId(boostingTableId: string): HotwordValidationResult {
if (!boostingTableId || boostingTableId.trim().length === 0) {
return { isValid: true, errors: [] };
}
const errors: string[] = [];
if (boostingTableId.length > 200) {
errors.push(`boosting_table_id 不能超过 200 个字符,当前为 ${boostingTableId.length}`);
}
if (!/^[a-zA-Z0-9_-]+$/.test(boostingTableId)) {
errors.push('boosting_table_id 只能包含字母、数字、下划线和连字符');
}
return { isValid: errors.length === 0, errors };
}

这些验证函数在用户配置热词时就会立即执行,确保问题在最早阶段被发现。对于用户体验来说,这种即时反馈是非常重要的。毕竟,用户输入的时候就知道哪里错了,总比提交后才发现要好。

在 HagiCode 的前端实现中,我们选择使用浏览器的 localStorage 来存储热词配置。这个设计决策背后有几点考量:首先,热词配置是非常个性化的设置,不同用户可能有不同的专业领域需求;其次,这种方式简化了后端实现,不需要额外的数据库表和 API 接口;最后,用户在浏览器中配置一次后,后续使用都能自动加载,非常方便。其实说白了,就是省事。

const HOTWORD_STORAGE_KEYS = {
contextText: 'hotword-context-text',
boostingTableId: 'hotword-boosting-table-id',
combineMode: 'hotword-combine-mode',
} as const;
export const DEFAULT_HOTWORD_CONFIG: HotwordConfig = {
contextText: '',
boostingTableId: '',
combineMode: false,
};
// 加载热词配置
export function loadHotwordConfig(): HotwordConfig {
const contextText = localStorage.getItem(HOTWORD_STORAGE_KEYS.contextText) || '';
const boostingTableId = localStorage.getItem(HOTWORD_STORAGE_KEYS.boostingTableId) || '';
const combineMode = localStorage.getItem(HOTWORD_STORAGE_KEYS.combineMode) === 'true';
return { contextText, boostingTableId, combineMode };
}
// 保存热词配置
export function saveHotwordConfig(config: HotwordConfig): void {
localStorage.setItem(HOTWORD_STORAGE_KEYS.contextText, config.contextText);
localStorage.setItem(HOTWORD_STORAGE_KEYS.boostingTableId, config.boostingTableId);
localStorage.setItem(HOTWORD_STORAGE_KEYS.combineMode, String(config.combineMode));
}

这段代码的逻辑非常简单清晰。加载配置时从 localStorage 读取,保存配置时写入 localStorage。我们还提供了默认配置,确保在没有任何配置时系统也能正常工作。毕竟,总得有个默认值吧。

在 HagiCode 的后端实现中,我们需要在 SDK 配置类中添加热词相关的属性。考虑到 C# 的语言特性和使用习惯,我们采用了 List<string> 来存储自定义热词上下文:

public class DoubaoVoiceConfig
{
/// <summary>
/// 应用 ID
/// </summary>
public string AppId { get; set; } = string.Empty;
/// <summary>
/// 访问令牌
/// </summary>
public string AccessToken { get; set; } = string.Empty;
/// <summary>
/// 服务 URL
/// </summary>
public string ServiceUrl { get; set; } = string.Empty;
/// <summary>
/// 自定义热词上下文列表
/// </summary>
public List<string>? HotwordContexts { get; set; }
/// <summary>
/// 豆包平台热词表 ID
/// </summary>
public string? BoostingTableId { get; set; }
}

这个配置类的设计遵循了 HagiCode 一贯的简洁风格。HotwordContexts 是可空的列表类型,BoostingTableId 是可空的字符串,这样在没有任何热词配置时,这些属性不会对请求造成任何影响。毕竟,不用的时候就不应该存在,这才叫干净。

Payload 的构建是整个热词功能的核心。当我们有了热词配置后,需要按照豆包 API 的要求格式化为正确的 JSON 结构。这个过程发生在 SDK 发送请求之前:

private void AddCorpusToRequest(Dictionary<string, object> request)
{
var corpus = new Dictionary<string, object>();
// 添加自定义热词
if (Config.HotwordContexts != null && Config.HotwordContexts.Count > 0)
{
corpus["context"] = new Dictionary<string, object>
{
["context_type"] = "dialog_ctx",
["context_data"] = Config.HotwordContexts
.Select(text => new Dictionary<string, object> { ["text"] = text })
.ToList()
};
}
// 添加平台热词表 ID
if (!string.IsNullOrEmpty(Config.BoostingTableId))
{
corpus["boosting_table_id"] = Config.BoostingTableId;
}
// 只有当 corpus 不为空时才添加到请求中
if (corpus.Count > 0)
{
request["corpus"] = corpus;
}
}

这段代码展示了如何根据配置动态构建 corpus 字段。关键点在于:只有当确实存在热词配置时,我们才会添加 corpus 字段。这种设计确保了向后兼容性——没有配置热词时,请求的结构与之前完全一致。毕竟,兼容性很重要,不能因为加个功能就把之前的逻辑搞乱了。

在前端和后端之间,热词参数通过 WebSocket 控制消息进行传递。HagiCode 的设计是:前端在开始录音时从 localStorage 加载热词配置,然后通过 WebSocket 消息发送给后端。

const controlMessage = {
type: 'control',
payload: {
command: 'StartRecognition',
contextText: '高血压\n糖尿病\n冠心病',
boosting_table_id: 'medical_table',
combineMode: false
}
};

这里有一个细节需要注意:前端传递的是多行文本(用换行符分隔),后端需要进行解析。后端的 WebSocket Handler 会解析这些参数并传递给 SDK:

private async Task HandleControlMessageAsync(
string connectionId,
DoubaoSession session,
ControlMessage message)
{
if (message.Payload is SessionControlRequest controlRequest)
{
// 解析热词参数
string? contextText = controlRequest.ContextText;
string? boostingTableId = controlRequest.BoostingTableId;
bool? combineMode = controlRequest.CombineMode;
// 解析多行文本为热词列表
if (!string.IsNullOrEmpty(contextText))
{
var hotwords = contextText
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.Where(s => s.Length > 0)
.ToList();
session.HotwordContexts = hotwords;
}
session.BoostingTableId = boostingTableId;
}
}

通过这样的设计,热词配置从前端到后端的传递变得清晰而高效。其实也没什么特别的,就是一层一层传下去而已。

在实际使用中,配置自定义热词非常简单。打开 HagiCode 的语音识别设置页面,找到”热词配置”区域。在”自定义热词文本”输入框中,每行输入一个热词短语。

比如,如果你正在开发一个医疗相关的应用,可以这样配置:

高血压
糖尿病
冠心病
心绞痛
心肌梗死
心力衰竭

保存配置后,每次开始语音识别时,这些热词都会自动传递给豆包 API。实际测试表明,配置热词后,相关专业术语的识别准确率有了明显提升。怎么说呢,效果还是有的,至少比之前好多了。

如果你需要管理大量的热词,或者热词需要频繁更新,那么平台热词表模式更适合你。首先需要在豆包自学习平台上创建热词表,获取生成的 boosting_table_id,然后在 HagiCode 的设置页面中输入这个 ID。

豆包自学习平台提供了热词的批量导入、分类管理等功能,对于需要管理大量专业术语的团队来说非常实用。通过平台管理热词,可以实现热词的集中维护和统一更新。毕竟,热词多了之后,有个地方统一管理,总比每次都要手动输入要好。

在某些复杂场景下,你可能需要同时使用自定义热词和平台热词表。这时只需要在 HagiCode 中同时配置两种热词,并开启”组合模式”开关。

组合模式下,豆包 API 会同时考虑两种热词来源,识别准确率通常比单独使用任意一种更高。不过需要注意的是,组合模式会增加请求的复杂度,建议在实际测试后再决定是否启用。毕竟,复杂度增加了,是不是真的值得,还是得看实际效果。

在 HagiCode 项目中集成热词功能非常简单。以下是一些常用的代码片段:

import {
loadHotwordConfig,
saveHotwordConfig,
validateHotwordConfig,
parseContextText,
getEffectiveHotwordMode,
type HotwordConfig
} from '@/types/hotword';
// 加载并验证配置
const config = loadHotwordConfig();
const validation = validateHotwordConfig(config);
if (!validation.isValid) {
console.error('热词配置验证失败:', validation.errors);
return;
}
// 解析热词文本
const hotwords = parseContextText(config.contextText);
console.log('解析到的热词:', hotwords);
// 获取有效的热词模式
const mode = getEffectiveHotwordMode(config);
console.log('当前热词模式:', mode);

后端的使用同样简洁:

var config = new DoubaoVoiceConfig
{
AppId = "your_app_id",
AccessToken = "your_access_token",
ServiceUrl = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async",
// 配置自定义热词
HotwordContexts = new List<string>
{
"高血压",
"糖尿病",
"冠心病"
},
// 配置平台热词表
BoostingTableId = "medical_table_v1"
};
var client = new DoubaoVoiceClient(config, logger);
await client.ConnectAsync();
await client.SendFullClientRequest();

在实现和使用热词功能时,有几点需要特别注意。

首先是字符限制。豆包 API 对热词有严格的限制,包括行数、每行字符数、总字符数等。如果超出限制,API 会返回错误。在 HagiCode 的前端实现中,我们通过验证函数在用户输入阶段就进行检查,避免将无效配置发送到后端。毕竟,提前发现问题,总比等 API 返回错误要好。

其次是 boosting_table_id 的格式。这个字段只允许字母、数字、下划线和连字符,不允许包含空格或其他特殊字符。在豆包自学习平台上创建热词表时,需要注意命名规范。其实这也难怪,API 对格式的要求总是比较严格的。

第三是向后兼容性。热词参数是完全可选的,不配置热词时,系统的工作方式与之前完全一致。这种设计确保了现有用户不会受到任何影响,也便于逐步迁移和升级。毕竟,不能因为加个功能就把之前的逻辑搞乱了。

最后是错误处理。当热词配置无效时,豆包 API 会返回相应的错误信息。HagiCode 的实现会记录详细的日志,便于开发者排查问题。同时,前端也会在界面上展示验证错误,帮助用户修正配置。错误处理做得好,用户体验自然也就好了。

通过本文的讲解,我们详细介绍了在 HagiCode 项目中实现豆包语音识别热词功能的完整方案。这套方案涵盖了从需求分析、技术选型到代码实现的全部环节,为开发者提供了可参考的实践范例。

核心要点可以归纳为以下几点:第一,豆包 API 支持自定义热词和平台热词表两种模式,可以独立使用也可以组合使用;第二,前端采用 localStorage 存储配置,简单高效;第三,后端通过动态构建 corpus 字段来传递热词参数,保持了良好的向后兼容性;第四,完善的验证逻辑确保了配置的正确性,避免了无效请求。怎么说呢,这套方案也不复杂,就是按照 API 的要求来而已。

热词功能的实现,让 HagiCode 在语音识别领域的能力得到了进一步增强。通过灵活配置业务相关的专业术语,开发者可以让语音识别系统更好地理解特定领域的内容,从而提供更加精准的服务。毕竟,技术最终是要服务业务的,能解决实际问题才是最重要的。

如果你觉得本文对你有帮助,欢迎来 GitHub 给个 Star 支持一下 HagiCode 项目。你的认可,是我们持续分享技术实践的动力。说到底,写文章分享技术,能帮到人,也算是种快乐了。


感谢您的阅读,如果您觉得本文有用,快点击下方点赞按钮👍,让更多的人看到本文。

本内容采用人工智能辅助协作,经本人审核,符合本人观点与立场。

AI Compose Commit:用 AI 智能重构 Git 提交工作流

AI Compose Commit:用 AI 智能重构 Git 提交工作流

Section titled “AI Compose Commit:用 AI 智能重构 Git 提交工作流”

在软件开发过程中,提交代码是程序员每天都要面对的日常工作。可是你有没有经历过这样的场景:一天工作结束后,打开 Git 看到几十个未暂存的修改文件,却不知道该如何将它们组织成合理的提交?

传统的方式是手动将文件分批暂存、逐个提交、撰写提交信息,这个过程既耗时又容易出错。咱们就常常在这上面浪费了不少时间,毕竟谁也不想在已经疲惫的晚上还要为这些琐事烦心。

我们在 HagiCode 项目中推出了一项新功能——AI Compose Commit,旨在彻底改变这个工作流程。它通过 AI 智能分析工作区中的所有未提交变更,自动将它们分组为多个逻辑提交,并执行符合规范的提交操作。本文将深入探讨这个功能的实现原理、技术架构以及我们在实践中遇到的挑战与解决方案。

本文分享的方案来自我们在 HagiCode 项目中的实践经验。

Git 作为版本控制系统,为开发者提供了强大的代码管理能力。但在实际使用中,提交操作往往成为开发流程中的瓶颈:

  1. 手动分组耗时: 当有大量文件变更时,开发者需要逐个检查文件内容,判断哪些属于同一个功能,这需要耗费大量脑力
  2. 提交信息质量参差: 撰写符合 Conventional Commits 规范的提交信息需要经验和技巧,新手常常写出不规范的提交
  3. 多仓库管理复杂: 在 monorepo 环境中,需要在不同仓库间切换,增加了操作复杂度
  4. 工作流被打断: 提交代码会打断开发思路,影响编码效率

这些问题在大型项目和团队协作环境中尤为明显。一个优秀的开发工具应该让开发者专注于核心的编码工作,而不是被繁琐的提交流程所困扰。

近年来,AI 技术在软件开发领域的应用日益广泛。从代码补全、错误检测到自动生成文档,AI 正在逐步渗透到开发的各个环节。在 Git 工作流方面,虽然已有一些工具提供提交信息生成的功能,但大多局限于单次提交的场景,缺乏对整个工作区变更的智能分析和分组能力。

其实 HagiCode 在开发过程中也遇到了这些痛点,我们曾尝试过多种工具,但都或多或少存在一些局限性。要么是功能不够完善,要么是用户体验不够好。这也是为什么我们最终决定自己实现 AI Compose Commit 功能的原因。

HagiCode 的 AI Compose Commit 功能正是为了填补这一空白而生,它不仅是生成提交信息,而是完整接管从文件分析到执行提交的整个流程。

在实现 AI Compose Commit 功能的过程中,我们面临了多个技术挑战:

  1. 文件语义理解: AI 需要理解文件变更的语义关系,判断哪些文件属于同一个功能模块。这需要深入分析文件内容、目录结构以及变更的上下文。

  2. 提交分组策略: 如何定义合理的分组标准?是按功能、按模块,还是按文件类型?不同的项目可能适用不同的策略。

  3. 实时反馈与异步处理: Git 操作可能需要较长时间,特别是处理大量文件时。如何在保证用户体验的同时完成复杂操作?

  4. 多仓库支持: 在 monorepo 架构下,需要在主仓库和子仓库之间正确路由操作。

  5. 错误处理与回滚: 如果某个提交失败,如何处理已执行的提交?是否需要回滚已暂存的文件?

  6. 提交信息一致性: 生成的提交信息需要符合项目现有的风格,保持历史提交的格式一致。

AI 处理大量文件变更会消耗显著的时间和计算资源。我们需要在以下方面进行优化:

  • 减少不必要的 AI 调用
  • 优化文件上下文的构建方式
  • 实现高效的 Git 操作批处理

这些问题在 HagiCode 的实际使用中都真实出现过,我们通过不断的迭代和优化才找到了相对完美的解决方案。如果你也在开发类似的工具,希望我们的经验能给你一些启发。

我们采用了分层架构来实现 AI Compose Commit 功能,确保系统具有良好的可扩展性和可维护性:

GitController 提供了 POST /api/git/auto-compose-commit 端点,作为功能入口。为了优化用户体验,我们采用了 Fire-and-Forget 异步模式:

  • 客户端发起请求后,服务器立即返回 HTTP 202 Accepted
  • 实际的 AI 处理在后台异步执行
  • 处理完成后通过 SignalR 通知客户端

这种设计确保了即使 AI 处理需要几分钟,用户也能立即得到响应,不会感觉系统卡顿。

GitAppService 负责核心业务逻辑:

  • 仓库检测:支持 monorepo 中的多仓库管理
  • 锁管理:防止并发操作导致的冲突
  • 文件暂存协调:与 AI 处理流程的交互
  • 错误回滚:处理失败场景下的状态恢复

AIGrain 作为 AI 操作的核心执行单元,实现了 IAIGrain 接口中的 AutoComposeCommitAsync 方法:

// 定义 AI 自动组合提交的接口方法
// 参数说明:
// - projectId: 项目唯一标识符
// - unstagedFiles: 未暂存文件列表,包含文件路径和状态信息
// - projectPath: 项目根目录路径(可选),用于访问项目上下文
// 返回值: 包含执行结果的响应对象,包括成功/失败状态和详细信息
[Alias("AutoComposeCommitAsync")]
[ResponseTimeout("00:20:00")] // 20 分钟超时,适用于处理大型变更集
Task<AutoComposeCommitResponseDto> AutoComposeCommitAsync(
string projectId,
GitFileStatusDto[] unstagedFiles,
string? projectPath = null);

这个方法设置了 20 分钟的超时时间,以处理大型变更集。HagiCode 在实际使用中发现,有些项目的单次变更可能涉及上百个文件,需要更长的处理时间。

通过抽象的 IAIService 接口,我们实现了 AI 服务的可插拔架构。目前使用 Claude Helper 服务,但可以轻松切换到其他 AI 提供商。

AI 需要了解每个文件的状态才能做出智能决策。我们通过 BuildFileChangesXml 方法构建文件上下文:

/// <summary>
/// 构建文件变更的 XML 表示形式,用于为 AI 提供完整的文件上下文信息
/// </summary>
/// <param name="stagedFiles">已暂存的文件列表,包含文件路径、状态和旧路径(针对重命名操作)</param>
/// <returns>格式化的 XML 字符串,包含所有文件的元数据信息</returns>
private static string BuildFileChangesXml(GitFileStatusDto[] stagedFiles)
{
var sb = new StringBuilder();
sb.AppendLine("<files>");
foreach (var file in stagedFiles)
{
sb.AppendLine(" <file>");
// 使用 XML 转义确保特殊字符不会破坏 XML 结构
sb.AppendLine($" <path>{System.Security.SecurityElement.Escape(file.Path)}</path>");
sb.AppendLine($" <status>{System.Security.SecurityElement.Escape(file.Status)}</status>");
// 处理文件重命名场景,记录旧路径以便 AI 理解变更关系
if (!string.IsNullOrEmpty(file.OldPath))
{
sb.AppendLine($" <oldPath>{System.Security.SecurityElement.Escape(file.OldPath)}</oldPath>");
}
sb.AppendLine(" </file>");
}
sb.AppendLine("</files>");
return sb.ToString();
}

这个 XML 格式的上下文包含文件路径、状态和旧路径(针对重命名操作),为 AI 提供了完整的元数据。通过结构化的 XML 格式,我们确保了 AI 能够准确理解每个文件的状态和变更类型。

为了让 AI 能够直接执行 Git 操作,我们配置了全面的工具权限:

// 定义 AI 可以使用的工具集合,包括文件操作和 Git 命令执行权限
// Read/Write/Edit: 文件读写和编辑能力
// Bash(git:*): 执行所有 Git 命令的权限
// 其他 Bash 命令: 用于查看文件内容和目录结构,辅助 AI 理解上下文
var allowedTools = new[]
{
"Read", "Write", "Edit",
"Bash(git:*)", "Bash(cat:*)", "Bash(ls:*)", "Bash(find:*)",
"Bash(grep:*)", "Bash(head:*)", "Bash(tail:*)", "Bash(wc:*)"
};
// 构建完整的 AI 请求对象
var request = new AIRequest
{
Prompt = prompt, // 完整的 Prompt 模板,包含任务指令和约束条件
WorkingDirectory = projectPath ?? GetTempDirectory(), // 工作目录,确保 AI 在正确的项目上下文中执行
AllowedTools = allowedTools, // 允许使用的工具集合
PermissionMode = PermissionMode.bypassPermissions, // 绕过权限检查,允许直接执行 Git 操作
LanguagePreference = languagePreference // 语言偏好设置,确保生成符合用户期望的提交信息
};

这里使用了 PermissionMode.bypassPermissions 模式,允许 AI 直接执行 Git 命令而无需用户确认。这是功能设计的核心,但同时也需要严格的输入验证来防止滥用。HagiCode 在实际部署中,通过后端的参数验证和日志监控,确保了这个机制的安全性。

AI 执行完成后,会返回结构化的结果。我们实现了双重解析策略以确保兼容性:

/// <summary>
/// 解析 AI 返回的提交执行结果,支持分隔符格式和正则表达式格式
/// </summary>
/// <param name="aiResponse">AI 返回的原始响应内容</param>
/// <returns>解析后的提交结果列表,每个结果包含提交哈希和执行状态</returns>
private List<CommitResultDto> ParseCommitExecutionResults(string aiResponse)
{
var results = new List<CommitResultDto>();
// 优先使用分隔符解析(新格式),这种格式更加明确和可靠
if (aiResponse.Contains("---"))
{
logger.LogDebug("Using delimiter-based parsing for AI response");
results = ParseDelimitedFormat(aiResponse);
if (results.Count > 0)
{
return results; // 成功解析,直接返回结果
}
logger.LogWarning("Delimiter-based parsing produced no results, falling back to regex");
}
else
{
logger.LogDebug("No delimiter found, using legacy regex-based parsing");
}
// 回退到正则表达式解析(旧格式),确保向后兼容性
return ParseLegacyFormat(aiResponse);
}

分隔符格式使用 --- 作为提交之间的分隔,格式清晰且易于解析:

---
Commit 1: abc123def456
feat(auth): add user login functionality
Implement JWT-based authentication with login form and API endpoints.
Co-Authored-By: Hagicode <noreply@hagicode.com>
---
Commit 2: 789ghi012jkl
docs(readme): update installation instructions
Add new setup steps for Docker environment.
Co-Authored-By: Hagicode <noreply@hagicode.com>
---

这种格式设计让解析变得简单可靠,同时人类阅读也很清晰。

为了防止并发操作导致的状态冲突,我们实现了仓库锁机制:

// 获取仓库锁,防止并发操作
// 参数说明:
// - fullPath: 仓库的完整路径,用于标识不同的仓库实例
// - requestedBy: 请求者标识,用于追踪和日志记录
await _autoComposeLockService.AcquireLockAsync(fullPath, requestedBy);
try
{
// 执行 AI Compose Commit 操作
// 这部分代码会调用 Orleans Grain 的方法,执行实际的 AI 处理和 Git 操作
await aiGrain.AutoComposeCommitAsync(projectId, unstagedFiles, projectPath);
}
finally
{
// 确保锁被释放,无论操作成功或失败
// 使用 finally 块可以保证异常情况下也能释放锁,避免死锁
await _autoComposeLockService.ReleaseLockAsync(fullPath);
}

锁具有 20 分钟的超时时间,与 AI 操作的超时设置保持一致。如果操作失败或超时,系统会自动释放锁,避免永久阻塞。HagiCode 在实际使用中发现,这个锁机制非常重要,特别是在团队协作环境中,多个开发者可能同时触发 AI Compose Commit 操作。

处理完成后,系统通过 SignalR 向前端发送通知:

/// <summary>
/// 发送自动组合提交完成的通知
/// </summary>
/// <param name="projectId">项目标识符,用于路由通知到正确的客户端</param>
/// <param name="totalCount">总提交数量,包括成功和失败</param>
/// <param name="successCount">成功提交的数量</param>
/// <param name="failureCount">失败提交的数量</param>
/// <param name="success">整体操作是否成功标志</param>
/// <param name="error">错误信息(如果操作失败)</param>
private async Task SendAutoComposeCommitNotificationAsync(
string projectId,
int totalCount,
int successCount,
int failureCount,
bool success,
string? error)
{
try
{
// 构建通知数据传输对象,包含详细的执行结果
var notification = new AutoComposeCommitCompletedDto
{
ProjectId = projectId,
TotalCount = totalCount,
SuccessCount = successCount,
FailureCount = failureCount,
Success = success,
Error = error
};
// 通过 SignalR Hub 广播通知到所有连接的客户端
await messageService.SendAutoComposeCommitCompletedAsync(notification);
logger.LogInformation(
"Auto compose commit notification sent for project {ProjectId}: {SuccessCount}/{TotalCount} succeeded",
projectId, successCount, totalCount);
}
catch (Exception ex)
{
// 记录通知错误但不影响主操作流程
// 通知失败不应该导致整个操作失败
logger.LogError(ex, "Failed to send auto compose commit notification for project {ProjectId}", projectId);
}
}

前端收到通知后可以更新 UI,显示提交成功或失败的状态,提升用户体验。这种实时反馈机制在 HagiCode 的使用中获得了很好的用户反馈,用户可以清楚地知道操作何时完成以及结果如何。

AI 的行为完全由 Prompt 决定,我们精心设计了 Auto Compose Commit 的 Prompt 模板。以中文版本为例(auto-compose-commit.zh-CN.hbs):

Prompt 开头明确声明支持非交互式运行模式,这是 CI/CD 和自动化脚本的关键需求:

**重要提示**:此提示词可能在非交互式环境中运行(如 CI/CD、自动化脚本)。
**非交互式模式**:
- 禁止使用 AskUserQuestion 或任何交互式工具
- 当需要用户输入时:
- 使用合理的默认值(如提交类型使用 feat)
- 跳过可选的确认步骤
- 记录所做的假设

这个设计确保了 AI Compose Commit 功能不仅能在交互式 IDE 环境中使用,也能集成到 CI/CD 流程中,实现完全自动化的提交流程。

为了防止 AI 执行危险操作,我们在 Prompt 中添加了严格的分支保护规则:

**分支保护**:
- 禁止执行任何分支切换操作(git checkout、git switch)
- 所有 git commit 命令必须在当前分支上执行
- 不得创建、删除或重命名分支
- 不得修改未跟踪文件或未暂存变更
- 如果需要分支切换才能完成操作,应返回错误而非执行

这些规则通过约束 AI 的工具使用范围,确保操作的安全性。HagiCode 在实际测试中验证了这些约束的有效性,AI 在遇到需要分支切换的场景时会安全地返回错误,而不是执行危险操作。

Prompt 中详细定义了文件分组的决策逻辑:

**文件分组决策树**:
├── 是否为配置文件(package.json、tsconfig.json、.env 等)?
│ ├── 是 → 独立提交(类型:chore 或 build)
│ └── 否 → 继续
├── 是否为文档文件(README.md、*.md、docs/**)?
│ ├── 是 → 独立提交(类型:docs)
│ └── 否 → 继续
├── 是否与同一功能相关?
│ ├── 是 → 合并到同一提交
│ └── 否 → 分别提交
└── 是否为跨模块变更?
├── 是 → 按模块分组
└── 否 → 按功能分组

这个决策树为 AI 提供了清晰的分组逻辑,确保生成的提交符合语义合理性。HagiCode 在实际使用中发现,这个决策树能够处理绝大多数常见场景,生成的分组结果符合开发者预期。

为了让提交信息与项目历史保持一致,Prompt 要求 AI 在生成前分析最近的提交历史:

**历史格式一致性**:在生成提交信息之前,你**必须**分析当前仓库的提交历史以匹配现有风格
1. 使用 git log -n 15 --pretty=format:"%H|%s|%b%n---%n" 获取最近的提交历史
2. 分析提交以识别:
- 结构模式:项目是否使用多段落?是否有 "Changes:" 或 "Capabilities:" 部分?
- 语言模式:提交信息是英文、中文还是混合?
- 常用类型:最常使用哪些提交类型(feat、fix、docs 等)?
- 特殊格式:是否有 Co-Authored-By 行?其他项目特定的约定?
3. 生成遵循检测到的模式的提交信息

这个分析确保了 AI 生成的提交信息不会显得突兀,而是与项目的提交历史保持风格一致。在 HagiCode 的多语言项目中,这个功能特别重要,它能够根据项目的提交历史自动选择合适的语言和格式。

每个提交必须包含 Co-Authored-By 信息:

**重要**:每个提交必须添加 Co-Authored-By 信息
- 使用以下格式:git commit -m "type(scope): subject" -m "" -m "Co-Authored-By: Hagicode <noreply@hagicode.com>"
- 或者直接在提交信息中包含 Co-Authored-By 行

这不仅是为了贡献规范,也是为了追踪 AI 辅助的提交历史。HagiCode 将这个要求作为强制规则,确保所有 AI 生成的提交都带有明确的来源标识。

完整的 AI Compose Commit 工作流程如下:

  1. 用户触发: 用户在 Git Status 面板或 Quick Actions Zone 点击”AI Auto Compose Commit”按钮
  2. API 请求: 前端发送 POST 请求到 /api/git/auto-compose-commit 端点
  3. 立即响应: 服务器返回 HTTP 202 Accepted,不等待处理完成
  4. 后台处理:
    • GitAppService 获取仓库锁
    • 调用 AIGrain 的 AutoComposeCommitAsync 方法
    • 构建文件上下文 XML
    • 执行 AI Prompt,让 AI 分析并执行提交
  5. AI 执行:
    • 使用 Git 命令获取所有未暂存变更
    • 读取文件内容理解变更性质
    • 按语义关系对文件分组
    • 对每组执行 git addgit commit 操作
  6. 结果解析: 解析 AI 返回的执行结果
  7. 通知发送: 通过 SignalR 通知前端
  8. 锁释放: 无论成功或失败,都释放仓库锁

这个流程的设计确保了用户可以在发起操作后立即继续其他工作,而不需要等待 AI 处理完成。HagiCode 的用户反馈表明,这种异步处理方式大大提升了工作流体验。

我们实现了多层级的错误处理:

// 验证请求参数的有效性,防止无效请求到达后端处理逻辑
if (request.UnstagedFiles == null || request.UnstagedFiles.Count == 0)
{
return BadRequest(new
{
message = "No unstaged files provided. Please make changes in the working directory first.",
status = "validation_failed"
});
}

如果 AI 处理过程中出现错误,系统会执行回滚操作,将已暂存的文件取消暂存,避免留下不一致的状态。这个机制在 HagiCode 的实际使用中挽救了多次意外中断,确保了仓库状态的完整性。

20 分钟的超时设置确保了长时间运行的操作不会无限期阻塞资源。超时后,系统会释放锁并通知用户操作失败。HagiCode 在实际使用中发现,大部分操作能够在 2-5 分钟内完成,只有处理超大型变更集时才会接近超时限制。

AI Compose Commit 最适合以下场景:

  • 一天工作结束后,批量处理多个文件的变更
  • 重构操作后,多个相关文件需要分别提交
  • 功能开发完成,需要将相关变更分组提交

不适合以下场景:

  • 单个文件的快速提交(直接使用普通提交更快)
  • 需要精确控制提交内容的场景
  • 包含敏感信息的提交(需要人工审核)

虽然 AI 智能分组很强大,但开发者仍应审查生成的提交:

  • 检查提交的分组是否符合预期
  • 验证提交信息的准确性
  • 确认没有遗漏或错误包含文件

如果发现不合理的分组,可以使用 git reset --soft HEAD~N 撤销后重新分组。HagiCode 的经验表明,即使 AI 分组很智能,人工审查仍然是有价值的,特别是在重要的功能提交时。

确保项目的 Git 配置支持 Conventional Commits:

Terminal window
# 安装 commitlint
npm install -g @commitlint/cli @commitlint/config-conventional
# 配置 commitlint
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

这样可以在 CI/CD 流程中验证提交信息格式,与 AI Compose Commit 生成的格式保持一致。

如果你想在项目中实现类似的 AI 辅助提交功能,以下是我们的建议:

先实现单次提交信息生成,再逐步扩展到多提交分组功能。这样更容易验证和迭代。HagiCode 也是按照这个路径逐步完善功能的,早期版本只支持单次提交,后来才扩展到多提交智能分组。

不要自己实现 AI 调用逻辑,使用现有的 SDK 可以减少开发时间和潜在 bug。我们使用了 Claude Helper 服务,它提供了稳定的接口和完善的错误处理。

Prompt 的质量直接决定了 AI 输出的质量。投入时间设计详细的 Prompt,包括:

  • 明确的任务描述
  • 具体的输出格式要求
  • 边界情况的处理规则
  • 示例说明

HagiCode 在 Prompt 设计上投入了大量时间,这是功能成功的关键因素之一。

AI 操作可能因为各种原因失败(网络问题、API 限流、内容审查等)。确保你的系统能够优雅地处理这些错误,并提供有意义的错误信息。

不要完全自动化,给用户保留控制权。提供查看分组结果、调整分组、手动编辑提交信息等选项,平衡自动化与灵活性。HagiCode 虽然实现了自动执行,但仍然保留了预览和调整的能力。

在构建文件上下文时,过滤掉不需要 AI 分析的文件:

// 过滤掉自动生成的文件和过大的文件,减少 AI 处理负担
var relevantFiles = stagedFiles
.Where(f => !IsGeneratedFile(f.Path))
.Where(f => !IsLargeFile(f.Path))
.ToArray();

如果支持多个独立仓库,可以并行处理不同仓库的提交,提高整体效率。

缓存项目提交历史分析结果,避免每次都重新分析。可以在配置文件中存储历史格式偏好,减少 AI 调用次数。

AI Compose Commit 功能代表了 AI 技术在软件开发工具中的深度应用。通过智能分析文件变更、自动分组提交、生成规范的提交信息,它显著提升了 Git 工作流的效率,让开发者能够更专注于核心的编码工作。

在实现过程中,我们学到了几个重要的经验:

  1. 用户反馈是关键: 早期版本采用同步等待方式,用户反馈体验不佳,改为 Fire-and-Forget 模式后满意度大幅提升
  2. Prompt 设计决定质量: 一个精心设计的 Prompt 比复杂的算法更能保证 AI 输出的质量
  3. 安全永远是第一位的: 虽然赋予 AI 直接执行 Git 命令的权限带来了效率提升,但必须配合严格的约束和验证
  4. 渐进式改进: 从简单场景开始,逐步增加复杂度,比一次性实现所有功能更容易成功

未来,我们计划进一步优化 AI Compose Commit 功能,包括:

  • 支持更多提交分组策略(按时间、按开发者等)
  • 集成代码审查流程,在提交前自动触发审查
  • 支持自定义提交信息模板,满足不同项目的个性化需求

如果你觉得本文分享的方案有价值,不妨也试试 HagiCode,体验一下这个功能在实际开发中的效果。毕竟实践是检验真理的唯一标准嘛。


感谢您的阅读,如果您觉得本文有用,快点击下方点赞按钮,让更多的人看到本文。

本内容采用人工智能辅助协作,经本人审核,符合本人观点与立场。