CrabNote螃蟹笔记

信鑫:基于大模型 + 知识库的 Code Review 实践

信鑫:基于大模型 + 知识库的 Code Review 实践

信鑫:基于大模型 + 知识库的 Code Review 实践 信鑫:基于大模型 + 知识库的 Code Review 实践 Modified March 1, 2024 加载知识库 CR Copilot 知识库分为『内置官方文档知识库』、『自定义知识库』,query 输入是先用完整代码截取前半段 + LLM 生成 summary 总结,然后和知识库做相似上下文,匹配流程如下: 截取完整代码前半段作为 query 输入,是因为大部分语言前半段都声明了 modules、packages,通过这种方式提高知识库相似匹配度。 官方文档 知识库(内置) 避免大家将官方文档重复录入、embedding,CR Copilot 内置了官方文档,目录包含: 并通过一个简单的 CURD 来管理内置向量知识库 自定义知识库 飞书文档(自定义) 飞书文档没有格式要求,能看懂正确代码是怎样就行 这里直接使用 LangChain 提供的 LarkSuite 文档加载类,对有权限的飞书文档进行获取,使用 CharacterTextSplitter / RecursiveCharacterTextSplitter 将文本分割成固定长度的块(chunks),方法有两个主要参数: • chunk size: 控制每个块的长度。例如设置为 1024,则每个块包含 1024 个字符。 • chunk overlap: 控制相邻两个块之间的重叠长度。例如设置为 128,则每个块会与相邻块重叠 128 个字符。 Prompt 指令设计 因为大模型有足够多的数据,我们想让大模型按要求执行就需要用到『Prompt 提示词』。 图源 Stephen Wolfram 代码 summary 总结指令 让 LLM 通过文件代码分析当前代码涉及的知识点,用于后续知识库相似度匹配: • prefix = "user: " if model == "chatglm2" else "<s Human: "suffix = "assistant(用中文): let's think step by step." if model == "chatglm2" else "\n</s <s Assistant(用中文): let's think step by step."return f"""{prefix}根据这段 {language} 代码,列出关于这段 {language} 代码用到的工具库、模块包。 {language} 代码: {source code} 请注意: 知识列表中的每一项都不要有类似或者重复的内容 列出的内容要和代码密切相关 最少列出 3 个, 最多不要超过 6 个 知识列表中的每一项要具体 列出列表,不要对工具库、模块做解释 输出中文 {suffix}""" 其中: • language:当前文件的代码语言(TypeScript、Python、Rust、Golang 等) • source code:是当前变更文件的完整代码 CR 指令 如果使用的模型(如 LLaMA 2)对中文 Prompt 支持较差,需要在设计 Prompt 时采用『输入英文』『输出中文』的方式,即: llama2f"""Human: please briefly review the {language}code changes by learning the provided context to do a brief code review feedback and suggestions. if any bug risk and improvement suggestion are welcome(no more than six) <context {context} </context <code changes {diff code} </code changes \n</s <s Assistant: """chatglm2f"""user: 【指令】请根据所提供的上下文信息来简要审查{language} 变更代码,进行简短的代码审查和建议,变更代码有任何 bug 缺陷和改进建议请指出(不超过 6 条)。 【已知信息】:{context} 【变更代码】:{diff code} assistant: """ 其中: • language:当前文件的代码语言(TypeScript、Python、Rust、Golang 等) • context:根据知识库返回的上下文信息 • diff code:是变更的代码(不使用完整代码主要是考虑 LLM max tokens 最大限制) 评论到变更代码行 为了能计算出变更代码行,写了一个函数,通过解析 diff 来输出变更的行数: Code block Python import redef parse diff(input):if not input:return []if not isinstance(input, str) or re.match(r"^\s+$", input):return []lines = input.split("\n")if not lines:return []result = []current file = Nonecurrent chunk = Nonedeleted line counter = 0added line counter = 0current file changes = Nonedef normal(line):nonlocal deleted line counter, added line countercurrent chunk["changes"].append({"type": "normal","normal": True,"ln1": deleted line counter,"ln2": added line counter,"content": line})deleted line counter += 1added line counter += 1current file changes["old lines"] = 1current file changes["new lines"] = 1def start(line):nonlocal current file, resultcurrent file = {"chunks": [],"deletions": 0,"additions": 0}result.append(current file)def to num of lines(number):return int(number) if number else 1def chunk(line, match):nonlocal current file, current chunk, deleted line counter, added line counter, current file changesif not current file:start(line)old start, old num lines, new start, new num lines = match.group(1), match.group(2), match.group(3), match.group(4)deleted line counter = int(old start)added line counter = int(new start)current chunk = {"content": line,"changes": [],"old start": int(old start),"old lines": to num of lines(old num lines),"new start": int(new start),"new lines": to num of lines(new num lines),}current file changes = {"old lines": to num of lines(old num lines),"new lines": to num of lines(new num lines),}current file["chunks"].append(current chunk)def delete(line):nonlocal deleted line counterif not current chunk:returncurrent chunk["changes"].append({"type": "del","del": True,"ln": deleted line counter,"content": line})deleted line counter += 1current file["deletions"] += 1current file changes["old lines"] = 1def add(line):nonlocal added line counterif not current chunk:returncurrent chunk["changes"].append({"type": "add","add": True,"ln": added line counter,"content": line})added line counter += 1current file["additions"] += 1current file changes["new lines"] = 1def eof(line):if not current chunk:returnmost recent change = current chunk["changes"][ 1]current chunk["changes"].append({"type": most recent change["type"],most recent change["type"]: True,"ln1": most recent change["ln1"],"ln2": most recent change["ln2"],"ln": most recent change["ln"],"content": line})header patterns = [(re.compile(r"^@@\s+ (\d+),?(\d+)?\s+\+(\d+),?(\d+)?\s@@"), chunk)]content patterns = [(re.compile(r"^\\ No newline at end of file$"), eof),(re.compile(r"^ "), delete),(re.compile(r"^\+"), add),(re.compile(r"^\s+"), normal)]def parse content line(line):nonlocal current file changesfor pattern, handler in content patterns:match = re.search(pattern, line)if match:handler(line)breakif current file changes["old lines"] == 0 and current file changes["new lines"] == 0:current file changes = Nonedef parse header line(line):for pattern, handler in header patterns:match = re.search(pattern, line)if match:handler(line, match)breakdef parse line(line):if current file changes:parse content line(line)else:parse header line(line)for line in lines:parse line(line)return result 这里机器人账号调用 Gitlab API 进行的评论,会默认被 Resolved,这样可以避免 CR Copilot 评论过多造成每个评论要手动点下 Resolved 一点感想 • 一切皆概率:基于 LLM 的应用最大特点在于『输出不确定性』,在候选词中选概率最高的进行输出,即使像 1+1=? 这样看起来有确定性输出的,LLM 也是基于概率给出的! • 开源 LLMs + 领域知识库 + 私有化部署是企业级应用的一种实践方式: ◦ 这里 LLMs 指多个大模型组合使用;大模型再强大也必须结合内部的知识库才能发挥作用; LarkSuite 加载知识库 CR Copilot 知识库分为『内置官方文档知识库』、『自定义知识库』,query 输入是先用完整代码截取前半段 + LLM 生成 summary 总结,然后和知识库做相似上下文,匹配流程如下: 截取完整代码前半段作为 query 输入,是因为大部分语言前半段都声明了 modules、packages,通过这种方式提高知识库相似匹配度。 截取完整代码前半段作为 query 输入,是因为大部分语言前半段都声明了 modules、packages,通过这种方式提高知识库相似匹配度。 官方文档 知识库(内置) 避免大家将官方文档重复录入、embedding,CR Copilot 内置了官方文档,目录包含: 并通过一个简单的 CURD 来管理内置向量知识库 自定义知识库 飞书文档(自定义) 飞书文档没有格式要求,能看懂正确代码是怎样就行 飞书文档没有格式要求,能看懂正确代码是怎样就行 这里直接使用 LangChain 提供的 LarkSuite 文档加载类,对有权限的飞书文档进行获取,使用 CharacterTextSplitter / RecursiveCharacterTextSplitter 将文本分割成固定长度的块(chunks),方法有两个主要参数: LarkSuite • chunk size: 控制每个块的长度。例如设置为 1024,则每个块包含 1024 个字符。 • chunk overlap: 控制相邻两个块之间的重叠长度。例如设置为 128,则每个块会与相邻块重叠 128 个字符。 Prompt 指令设计 因为大模型有足够多的数据,我们想让大模型按要求执行就需要用到『Prompt 提示词』。 图源 Stephen Wolfram 代码 summary 总结指令 让 LLM 通过文件代码分析当前代码涉及的知识点,用于后续知识库相似度匹配: • prefix = "user: " if model == "chatglm2" else "<s Human: "suffix = "assistant(用中文): let's think step by step." if model == "chatglm2" else "\n</s <s Assistant(用中文): let's think step by step."return f"""{prefix}根据这段 {language} 代码,列出关于这段 {language} 代码用到的工具库、模块包。 {language} 代码: {source code} 请注意: 知识列表中的每一项都不要有类似或者重复的内容 列出的内容要和代码密切相关 最少列出 3 个, 最多不要超过 6 个 知识列表中的每一项要具体 列出列表,不要对工具库、模块做解释 输出中文 {suffix}""" 其中: • language:当前文件的代码语言(TypeScript、Python、Rust、Golang 等) • source code:是当前变更文件的完整代码 CR 指令 如果使用的模型(如 LLaMA 2)对中文 Prompt 支持较差,需要在设计 Prompt 时采用『输入英文』『输出中文』的方式,即: llama2f"""Human: please briefly review the {language}code changes by learning the provided context to do a brief code review feedback and suggestions. if any bug risk and improvement suggestion are welcome(no more than six) <context {context} </context <code changes {diff code} </code changes \n</s <s Assistant: """chatglm2f"""user: 【指令】请根据所提供的上下文信息来简要审查{language} 变更代码,进行简短的代码审查和建议,变更代码有任何 bug 缺陷和改进建议请指出(不超过 6 条)。 【已知信息】:{context} 【变更代码】:{diff code} assistant: """ 其中: • language:当前文件的代码语言(TypeScript、Python、Rust、Golang 等) • context:根据知识库返回的上下文信息 • diff code:是变更的代码(不使用完整代码主要是考虑 LLM max tokens 最大限制) 评论到变更代码行 为了能计算出变更代码行,写了一个函数,通过解析 diff 来输出变更的行数: 这里机器人账号调用 Gitlab API 进行的评论,会默认被 Resolved,这样可以避免 CR Copilot 评论过多造成每个评论要手动点下 Resolved 这里机器人账号调用 Gitlab API 进行的评论,会默认被 Resolved,这样可以避免 CR Copilot 评论过多造成每个评论要手动点下 Resolved 一点感想 • 一切皆概率:基于 LLM 的应用最大特点在于『输出不确定性』,在候选词中选概率最高的进行输出,即使像 1+1=? 这样看起来有确定性输出的,LLM 也是基于概率给出的! • 开源 LLMs + 领域知识库 + 私有化部署是企业级应用的一种实践方式: ◦ 这里 LLMs 指多个大模型组合使用;大模型再强大也必须结合内部的知识库才能发挥作用; ◦ 这里 LLMs 指多个大模型组合使用;大模型再强大也必须结合内部的知识库才能发挥作用; ◦ 私有化部署好处是打消各行各业对数据安全的担忧! ◦ 大模型在 Chat 聊天的产品形态更多是秀肌肉 ,让各行各业能被触达到;最终的产品形态需要具体场景具体分析! • AI+ 刚刚开始:CR Copilot 只是达人 LLMs + 研发工程化其中一个应用场景,还有一些应用/工具等达人团队打磨好后再和大家一起分享! 📗 作者:信鑫 原文:https://zhuanlan.zhihu.com/p/655550057 作者:信鑫 原文:https://zhuanlan.zhihu.com/p/655550057 抛砖引玉,作者对大模型 LLMs 认知有限,有不当之处望大家指正! 抛砖引玉,作者对大模型 LLMs 认知有限,有不当之处望大家指正! 背景 想法源于在一次 Code Review 时,向 Claude 询问哪种写法代码更优雅得来。当时就想能不能让 AI 帮我们辅助做 Code Review? 痛点 • 安全合规问题:公司内代码直接调 ChatGPT / Claude 会有安全/合规问题,为了使用 ChatGPT / Claude 需要对代码脱敏,只提供抽象逻辑,这往往更花时间。 ◦ 三星引入ChatGPT不到 20 天,被曝发生3次芯片机密泄露 三星引入ChatGPT不到 20 天,被曝发生3次芯片机密泄露 ◦ 三星引入ChatGPT不到 20 天,被曝发生3次芯片机密泄露 三星引入ChatGPT不到 20 天,被曝发生3次芯片机密泄露 • 低质量代码耗费时间:达人业务每天至少 10 20 个 MR 需要 CR,虽然提交时 MR 经过 单测 + Lint 过滤了一些低级错误,但还有些问题(代码合理性、经验、MR 相关业务逻辑等)需要花费大量时间,最后可以先经过一轮自动化 CR,再进行人工 CR,可大大提升 CR 效率! • 团队 Code Review 规范缺少执行:大部分团队的 Code Review 停留在文档纸面上,成员之间口口相传,并没有一个工具根据规范来严格执行。 介绍 一句话介绍就是:基于开源大模型 + 知识库的 Code Review 实践,类似一个代码评审助手(CR Copilot)。 特性 符合公司安全规范,所有代码数据不出内网,所有推理过程均在内网完成 符合公司安全规范,所有代码数据不出内网,所有推理过程均在内网完成 • 开箱即用:基于 Gitlab CI,仅 10 几行配置完成接入,即可对 MR 进行 CR。 • 数据安全:基于开源大模型做私有化部署,隔离外网访问,确保代码 CR 过程仅在内网环境下完成。 • ♾ 无调用次数限制:部署在内部平台,只有 GPU 租用成本。 • 自定义知识库:CR 助手基于提供的飞书文档进行学习,将匹配部分作为上下文,结合代码变更进行 CR,这将大大提升 CR 的准确度,也更符合团队自身的 CR 规范。 • 评论到变更行:CR 助手将结果评论到变更代码行上,通过 Gitlab CI 通知,更及时获取 CR 助手给出的评论。 名词解释 实现思路 流程图 系统架构 完成一次 CR 流程,需要用到如下技术模块: LLMs / 开源大模型选型 CR Copilot 功能的核心在于大语言模型基座,基于不同大模型基座生成的 CR 质量也不尽相同。对于 CR 这个场景,我们需要选型的模型满足以下几个条件: • 理解代码 • 对中文支持好 • 较强的上下文学习能力 FlagEval 8 月大模型评测榜单(https://flageval.baai.ac.cn/ /trending) 模型后面的 {n}b 指 n 10 亿参数量,比如 13b 就是 130 亿参数,个人试用下来参数量的多少并不能决定效果怎样,根据实际情况来判断。 模型后面的 {n}b 指 n 10 亿参数量,比如 13b 就是 130 亿参数,个人试用下来参数量的多少并不能决定效果怎样,根据实际情况来判断。 起初在众多大模型中选择『gemma 7b it』、『Llama2 Chinese 13b Chat』和『chatglm2 6b』、『Baichuan2 13B Chat』,通过一段时间模型赛马 ,主观上感觉 Gemma、Llama2 会更适用于 CR 场景,而 ChatGLM2 更像是文科生,对代码评审没有太多建设性建议,但在中文 AIGC 上会比较有优势! gemma 7b it Llama2 Chinese 13b Chat chatglm2 6b Baichuan2 13B Chat 两个模型执行过程中的记录 因大模型合规问题,CR Copilot 会默认使用 Gemma,可选使用 ChatGLM,如有使用 Llama2 模型需求需要向 Meta 申请,通过后可使用。 Meta 申请 Llama 2 要求企业的月活用户数不超过 7 亿 目前支持的模型可选,仅供参考的评分如下: 评分 • chatglm2 6b(默认) chatglm2 6b • Llama2 Chinese 13b Chat(推荐) Llama2 Chinese 13b Chat • Baichuan2 13B Chat Baichuan2 13B Chat 知识库设计 为什么需要知识库? 大模型基座只包含互联网上的公开数据,对公司内部的框架知识和使用文档并不了解。 举个例子 :公司内有个框架叫 Lynx,让大模型从内部文档中知道『什么是 Lynx?』、『怎么写 Lynx?』 一图胜千言 这里的『强化模式』会使用向量数据库,并将匹配的知识库片段和问题『什么是 Lynx?』生成 Prompt,发送到 LLM 执行。 这里的『强化模式』会使用向量数据库,并将匹配的知识库片段和问题『什么是 Lynx?』生成 Prompt,发送到 LLM 执行。 怎样找到相关度高的知识? 有了知识库后,怎样将我们『搜索的问题/代码』在『知识库』中找到『相关度最高的内容』? 答案是通过三个过程: 1. Text Embeddings(文本向量化) 2. Vector Stores(向量存储) 3. Similarity Search(相似性搜索) 文本相似度匹配流程图,图源 Langchain Chatchat Text Embeddings(文本向量化) 不同于传统数据库的模糊搜索/匹配关键字,我们需要进行语义/特征匹配。 例如:你搜索『猫』,只能得到带 『猫』 关键字匹配的结果,没办法得到 『布偶』、『蓝白』 等结果,传统数据库认为『布偶』是『布偶』、『猫』是『猫』。要实现关联语义搜索,是通过人工打特征标签,这个过程也被称为特征工程(Feature Engineering)。 如何才能将文本自动化的方式来提取这些特征?这就要通过 Vector Embedding 向量化实现,目前社区通过 OpenAI 提供的 text embedding ada 002 模型生成,这会引起两个问题: 1. 数据安全问题:需要调用 OpenAI 的 API 才能做向量化 2. 收费:大概 3000页/美元 我们使用了国产文本相似度计算模型 bge large zh,并私有化部署公司内网,一次 embedding 向量化耗时基本在毫秒级。 bge large zh Vector Stores(向量存储) 提前将官方文档进行 Vector Embeddings,然后存储在向量数据库里,我们这里选择的向量数据库是 Qdrant,主要考虑到是用 Rust 写的,存储和查询也许会快一些!这里引用一个向量数据库选型的几个维度选择: 数据截止到2023年9月10号 数据截止到2023年9月10号 Similarity Search(相似性搜索) 原理是通过比较向量之间的距离来判断它们的相似度 那么有了『query 问题的向量』和『数据库里录入的知识库向量』后,这可以直接使用向量数据库提供的 Similarity Search 方式匹配相关内容。