Im Jahr 2023 wird die Popularität der generativen KI dazu führen, dass immer mehr Unternehmen KI-gestützte Codierung einführen. Der geringfügige Unterschied zum 2021 veröffentlichten GitHub Copilot besteht darin, dass die Codevervollständigung nur eines von vielen Szenarios ist. Eine große Anzahl von Unternehmen erforscht Szenarien wie die Generierung vollständiger Codes und Codeüberprüfungen basierend auf Anforderungen und führt außerdem generative KI ein, um die Entwicklungseffizienz zu verbessern.
In diesem Zusammenhang haben wir (die Open-Source-Community von Thoughtworks) auch eine Reihe von KI-Hilfstools als Open-Source-Lösung bereitgestellt, um mehr Organisationen beim Aufbau ihrer eigenen KI-gestützten Codierungsassistenten zu unterstützen:
Denn als wir AutoDev entwickelten, wurden verschiedene Open-Source-Modelle ständig weiterentwickelt. In diesem Zusammenhang sind die Schritte:
Daher konzentriert sich dieses Tutorial auch auf diese drei Schritte. Basierend auf unserer Erfahrung ist außerdem der Beispiel-Technologie-Stack für dieses Tutorial:
Da unsere Erfahrung mit KI relativ begrenzt ist, wird es zwangsläufig einige Fehler geben. Daher hoffen wir auch, mit mehr Entwicklern zusammenzuarbeiten, um dieses Open-Source-Projekt zu erstellen.
In Kombination mit dem Teil zur künstlichen Intelligenz des JetBrains 2023 „Developer Ecosystem“-Berichts können wir einige allgemeine Szenarien zusammenfassen, die die Bereiche widerspiegeln, in denen generative KI eine Rolle im Entwicklungsprozess spielen kann. Hier sind einige der Hauptszenarien:
Als wir AutoDev erstellten, entdeckten wir auch Szenarien wie das Erstellen von SQL-DDL, das Generieren von Anforderungen, TDD usw. Also. Wir bieten die Möglichkeit, Szenarien anzupassen, sodass Entwickler ihre eigenen KI-Funktionen anpassen können. Weitere Informationen finden Sie unter: https://ide.unitmesh.cc/customize.
Im täglichen Coding gibt es mehrere unterschiedliche Szenarien mit unterschiedlichen Anforderungen an die KI-Reaktionsgeschwindigkeit (nur als Beispiel):
Szene | Reaktionsgeschwindigkeit | Qualitätsanforderungen generieren | Größe erwartet | veranschaulichen |
---|---|---|---|---|
Code-Vervollständigung | schnell | Mitte | 1~6B | Die Codevervollständigung ist das häufigste Szenario beim täglichen Codieren und die Reaktionsgeschwindigkeit ist entscheidend. |
Dokumentenerstellung | Mitte | Mitte | 1 | Für die Erstellung der Dokumentation ist ein umfassendes Verständnis der Codestruktur erforderlich. Geschwindigkeit und Qualität sind gleichermaßen wichtig. |
Codeüberprüfung | schnell | Mitte | 1 | Codeüberprüfungen erfordern eine qualitativ hochwertige Beratung, müssen aber auch so reaktionsschnell wie möglich sein. |
Generierung von Unit-Tests | schnell | Mitte | 6B~ | Unit-Tests generieren weniger Kontext und Reaktionsfähigkeit und KI-Qualität sind gleichermaßen wichtig. |
Code-Refactoring | Mitte | hoch | 32B~ | Die Umgestaltung des Codes erfordert möglicherweise ein besseres Kontextverständnis und die Reaktionszeiten können sich geringfügig verlangsamen. |
Nachfragegenerierung | Mitte | hoch | 32B~ | Die Nachfragegenerierung ist ein relativ komplexes Szenario und die Reaktionsgeschwindigkeit kann leicht verlangsamt werden, um Genauigkeit sicherzustellen. |
Suche und Interpretation von Codes in natürlicher Sprache | Mittel-Niedrig | hoch | 32B~ | Die Suche und Interpretation von Code in natürlicher Sprache ist ein relativ komplexes Szenario, und die Reaktionsgeschwindigkeit kann leicht verlangsamt werden, um die Genauigkeit sicherzustellen. |
PS: Die 32B werden hier nur als Größenordnung ausgedrückt, da der Effekt bei einem größeren Modell besser ist.
Daher fassen wir es wie folgt zusammen: ein großes, ein mittleres, ein Mikro- und drei Modelle, die eine umfassende KI-gestützte Codierung bieten:
Die KI-Codevervollständigung kann IDE-Tools kombinieren, um den Codekontext und die Regeln der Programmiersprache zu analysieren, und die KI generiert automatisch Codeausschnitte oder schlägt sie vor. In Code-Vervollständigungstools, die GitHub Copilot ähneln, sind sie normalerweise in drei Unterteilungsmodi unterteilt:
Inline-Vervollständigung (Inline)
Ähnlich wie beim FIM-Modus (in der Mitte ausfüllen) befindet sich der fertige Inhalt in der aktuellen Zeile. Beispiel: BlotPost blogpost = new
, die Vervollständigung lautet: BlogPost();
um Folgendes zu erreichen: BlogPost blogpost = new BlogPost();
Wir können Deepseek Coder als Beispiel verwenden, um den Effekt in diesem Szenario zu sehen:
< |fim▁begin| > def quick_sort(arr):
if len(arr) < = 1:
return arr
pivot = arr[0]
left = []
right = []
< |fim▁hole| >
if arr[i] < pivot:
left.append(arr[i])
else:
right.append(arr[i])
return quick_sort(left) + [pivot] + quick_sort(right) < |fim▁end| >
Hier müssen wir den Code vor und nach dem Cursor kombinieren.
In-Block-Abschluss (InBlock)
Durch Kontextlernen (In-Context-Lernen) erreicht, befindet sich der Abschlussinhalt im aktuellen Funktionsblock. Der Originalcode lautet beispielsweise:
fun createBlog ( blogDto : CreateBlogDto ): BlogPost {
}
Der fertige Code lautet:
val blogPost = BlogPost (
title = blogDto.title,
content = blogDto.content,
author = blogDto.author
)
return blogRepository.save(blogPost)
AfterBlock
Durch Kontextlernen (In-Context-Lernen) wird der Abschluss nach dem aktuellen Funktionsblock erreicht, z. B.: Abschluss einer neuen Funktion nach dem aktuellen Funktionsblock. Der Originalcode lautet beispielsweise:
fun createBlog ( blogDto : CreateBlogDto ): BlogPost {
// ...
}
Der fertige Code lautet:
fun updateBlog ( id : Long , blogDto : CreateBlogDto ): BlogPost {
// ...
}
fun deleteBlog ( id : Long ) {
// ...
}
Wenn wir die entsprechende KI-Vervollständigungsfunktion erstellen, müssen wir auch erwägen, sie auf den entsprechenden Musterdatensatz anzuwenden, um die Qualität der Vervollständigung zu verbessern und eine bessere Benutzererfahrung zu bieten.
Einige verwandte Ressourcen zum Schreiben dieses Artikels:
Codeerklärungen sollen Entwicklern dabei helfen, große Codebasen effektiver zu verwalten und zu verstehen. Diese Assistenten können Fragen zur Codebasis beantworten, Dokumentation bereitstellen, Code durchsuchen, Fehlerquellen identifizieren, Codeduplizierung reduzieren usw. und so die Entwicklungseffizienz verbessern, Fehlerraten reduzieren und die Arbeitsbelastung der Entwickler verringern.
In diesem Szenario besteht es je nach erwarteter Generierungsqualität normalerweise aus zwei Modellen: einem großen und einem Mikromodell oder einem mittleren und einem Mikromodell. Das größere Modell erzielt bessere Ergebnisse hinsichtlich der Generierungsqualität. In Kombination mit unserer Designerfahrung im Chocolate Factory-Tool lässt sich eine solche Funktion normalerweise in mehrere Schritte unterteilen:
Als RAG-Anwendung ist sie in zwei Teile unterteilt: Indizierung und Abfrage.
In der Indizierungsphase müssen wir die Codebasis indizieren, was Textsegmentierung, Vektorisierung, Datenbankindizierung und andere Technologien umfasst. Eines der schwierigsten Elemente ist die Aufteilung. Die Aufteilungsregeln, auf die wir uns beziehen, sind: https://docs.sweep.dev/blogs/chunking-2m-files. Im Augenblick:
In verschiedenen Szenarien können wir auch auf unterschiedliche Weise teilen. In Chocolate Factory teilen wir beispielsweise durch AST, um die Qualität des generierten Kontexts sicherzustellen.
In der Abfragephase müssen wir einige unserer traditionellen Suchtechnologien kombinieren, wie z. B. Vektorisierungssuche, Pfadsuche usw., um die Qualität der Suche sicherzustellen. Gleichzeitig müssen wir im chinesischen Szenario auch die Frage der Konvertierung ins Chinesische berücksichtigen, beispielsweise die Konvertierung von Englisch ins Chinesische, um die Qualität der Suche sicherzustellen.
Zur täglichen Unterstützung können wir dies auch durch generative KI erreichen, z. B. durch automatisches Erstellen von SQL-DDL, automatisches Erstellen von Testfällen, automatisches Erstellen von Anforderungen usw. Dies kann nur durch die Anpassung von Aufforderungswörtern und die Kombination spezifischer Domänenkenntnisse erreicht werden, daher werde ich hier nicht auf Details eingehen.
Neben dem Modell ist auch der Kontext ein wichtiger Faktor, der die KI-Unterstützungsfähigkeiten beeinflusst. Als wir AutoDev erstellt haben, haben wir auch zwei verschiedene Kontextmodi entdeckt:
Ein einfacher Vergleich sieht wie folgt aus:
relevanten Kontext | ähnlichen Kontext | |
---|---|---|
Suchtechnologie | Statische Code-Analyse | Ähnlichkeitssuche |
Informationen zur Datenstruktur | AST, CFG | Ähnlicher Brocken |
Plattformübergreifende Funktionen | Hängt von der IDE oder dem unabhängigen Parser ab | Nicht abhängig von bestimmten Plattformen |
kontextuelle Qualität | extrem hoch | hoch |
Ergebnisse generieren | extrem hoch | hoch |
Baukosten | Abhängig von Sprache und Plattform | Niedrig |
Wenn die IDE-Unterstützung begrenzt ist, führt die kontextbezogene Implementierung zu einer höheren Kostenleistung .
GitHub Copilot verwendet ein ähnliches Kontextarchitekturmuster und seine detaillierte Architektur ist wie folgt geschichtet:
In den Forschungsmaterialien des „öffentlichen“ Copilot-Explorer-Projekts können Sie sehen, wie Prompt aufgebaut ist. Das Folgende ist die prompte Anfrage, die an folgende Adresse gesendet wurde:
{
"prefix" : " # Path: codeviz \ app.py n #.... " ,
"suffix" : " if __name__ == '__main__': rn app.run(debug=True) " ,
"isFimEnabled" : true ,
"promptElementRanges" : [
{
"kind" : " PathMarker " ,
"start" : 0 ,
"end" : 23
},
{
"kind" : " SimilarFile " ,
"start" : 23 ,
"end" : 2219
},
{
"kind" : " BeforeCursor " ,
"start" : 2219 ,
"end" : 3142
}
]
}
In:
prefix
zum Erstellen der Eingabeaufforderung verwendete Präfixteil wird aus promptElements erstellt, darunter: BeforeCursor
, AfterCursor
, SimilarFile
, ImportedFile
, LanguageMarker
, PathMarker
, RetrievalSnippet
und andere Typen. Aus den Namen mehrerer PromptElementKind
können wir auch ihre wahre Bedeutung erkennen.suffix
zum Erstellen der Eingabeaufforderung verwendete Suffixteil wird durch den Teil bestimmt, in dem sich der Cursor befindet, und durch die Anzahl der verbleibenden Positionen, die basierend auf der Obergrenze der Token (2048) berechnet werden müssen. Die Token-Berechnung hier ist die echte LLM-Token-Berechnung. In Copilot wird sie von Cushman002 berechnet. Die Token-Länge chinesischer Zeichen ist unterschiedlich, z. B.: { context: "console.log('你好,世界')", lineCount: 1, tokenLength: 30 }
, wobei die Länge des Inhalts im Kontext 20 beträgt, aber tokenLength ist 30, die Länge chinesischer Zeichen beträgt 5 (einschließlich ,
) und der von einem einzelnen Zeichen belegte Token ist 3.Hier ist ein detaillierteres Beispiel eines Java-Anwendungskontexts:
// Path: src/main/cc/unitmesh/demo/infrastructure/repositories/ProductRepository.java
// Compare this snippet from src/main/cc/unitmesh/demo/domain/product/Product.java:
// ....
// Compare this snippet from src/main/cc/unitmesh/demo/application/ProductService.java:
// ...
// @Component
// public class ProductService {
// //...
// }
//
package cc . unitmesh . demo . repositories ;
// ...
@ Component
public class ProductRepository {
//...
Im Computerkontext verwendet GitHub Copilot den Jaccard-Koeffizienten (Jaccard-Ähnlichkeit). Weitere Informationen zur Logik finden Sie unter: Nach mehr als einem halben Monat habe ich Github Copilot schließlich rückentwickelt.
Verwandte Ressourcen:
Wie oben erwähnt, basiert der relevante Code auf der statischen Codeanalyse , hauptsächlich mithilfe von Strukturinformationen des Codes wie AST, CFG, DDG usw. In verschiedenen Szenarien und Plattformen können wir verschiedene statische Code-Analysetools kombinieren. Im Folgenden sind einige gängige statische Code-Analysetools aufgeführt:
Im Abschlussszenario können wir durch statische Codeanalyse den aktuellen Kontext abrufen, z. B.: aktuelle Funktion, aktuelle Klasse, aktuelle Datei usw. Das Folgende ist ein Beispiel für den AutoDev-Kontext zum Generieren von Komponententests:
// here are related classes:
// 'filePath: /Users/phodal/IdeaProjects/untitled/src/main/java/cc/unitmesh/untitled/demo/service/BlogService.java
// class BlogService {
// blogRepository
// + public BlogPost createBlog(BlogPost blogDto)
// + public BlogPost getBlogById(Long id)
// + public BlogPost updateBlog(Long id, BlogPost blogDto)
// + public void deleteBlog(Long id)
// }
// 'filePath: /Users/phodal/IdeaProjects/untitled/src/main/java/cc/unitmesh/untitled/demo/dto/CreateBlogRequest.java
// class CreateBlogRequest ...
// 'filePath: /Users/phodal/IdeaProjects/untitled/src/main/java/cc/unitmesh/untitled/demo/entity/BlogPost.java
// class BlogPost {...
@ ApiOperation ( value = "Create a new blog" )
@ PostMapping ( "/" )
public BlogPost createBlog ( @ RequestBody CreateBlogRequest request ) {
In diesem Beispiel wird der Kontext createBlog
analysiert, um die Eingabe- und Ausgabeklassen der Funktion zu erhalten: CreateBlogRequest
, BlogPost
-Informationen und „BlogService“-Klasseninformationen, die dem Modell als Kontext (bereitgestellt in Kommentaren) bereitgestellt werden. An diesem Punkt generiert das Modell genauere Konstruktoren sowie genauere Testfälle.
Da der relevante Kontext auf der statischen Codeanalyse verschiedener Sprachen und APIs verschiedener IDEs beruht, müssen wir uns auch an verschiedene Sprachen und verschiedene IDEs anpassen. Hinsichtlich der Baukosten ist es im Vergleich zu ähnlichen Kontexten teurer.
IDEs und Editoren sind die wichtigsten Werkzeuge für Entwickler, und ihre Design- und Lernkosten sind relativ hoch. Zunächst können wir die offizielle Vorlage verwenden, um Folgendes zu generieren:
Dann noch Funktionalität hinzufügen (ist das nicht ganz einfach), natürlich nicht. Im Folgenden finden Sie einige IDEA-Plug-in-Ressourcen, auf die Sie verweisen können:
Natürlich ist es angemessener, auf das AutoDev-Plug-In zu verweisen.
Sie können die offizielle Vorlage direkt verwenden, um das entsprechende Plug-In zu generieren: https://github.com/JetBrains/intellij-platform-plugin-template
Die Implementierung des IDEA-Plug-Ins erfolgt hauptsächlich über Action und Listener, die nur in plugin.xml
registriert werden müssen. Einzelheiten finden Sie in der offiziellen Dokumentation: IntelliJ Platform Plugin SDK
Da wir das Kompatibilitätsproblem mit IDE-Versionen von AutoDev in der Anfangsphase nicht berücksichtigt haben, müssen wir eine Kompatibilitätsverarbeitung für das Plug-in durchführen, um später mit älteren IDE-Versionen kompatibel zu sein. Daher können wir, wie im offiziellen Dokument „Build Number Ranges“ beschrieben, sehen, dass verschiedene Versionen unterschiedliche Anforderungen an das JDK haben. Im Folgenden sind die Anforderungen für verschiedene Versionen aufgeführt:
Filialnummer | IntelliJ-Plattformversion |
---|---|
233 | 2023.3 |
232 | 2023.2 |
231 | 2023.1 |
223 | 2022.3 |
222 | 2022.2 HINWEIS Java 17 ist jetzt erforderlich (Blogbeitrag) |
221 | 2022.1 |
213 | 2021.3 |
212 | 2021.2 |
211 | 2021.1 |
203 | 2020.3 HINWEIS Java 11 ist jetzt erforderlich (Blogbeitrag) |
Und konfigurieren Sie es in gradle.properties
:
pluginSinceBuild = 223
pluginUntilBuild = 233.*
Die nachträgliche Konfiguration der Kompatibilität ist mühsam, daher können Sie sich auf das Design von AutoDev beziehen.
In Bezug auf die automatische Code-Vervollständigung verweisen inländische Hersteller hauptsächlich auf die Implementierung von GitHub Copilot, und die Logik ist nicht kompliziert.
Auslösen mit Tastenkombinationen
Es überwacht hauptsächlich Benutzereingaben in Action und dann:
Funktion | Tastenkombination | veranschaulichen |
---|---|---|
requestCompletions | Alt + / | Rufen Sie den aktuellen Kontext ab und rufen Sie dann die Abschlussergebnisse über das Modell ab |
applyInlays | TAB | Abschlussergebnisse in der IDE anzeigen |
entsorgenInlays | ESC | Abschluss abbrechen |
CycleNextInlays | Alt + ] | Wechseln Sie zum nächsten Abschlussergebnis |
CyclePrevInlays | Alt + [ | Wechseln Sie zum vorherigen Abschlussergebnis |
Verwenden Sie die automatische Auslösemethode
Es überwacht hauptsächlich Benutzereingaben über EditorFactoryListener
und löst dann basierend auf unterschiedlichen Eingaben unterschiedliche Abschlussergebnisse aus. Der Kerncode lautet wie folgt:
class AutoDevEditorListener : EditorFactoryListener {
override fun editorCreated ( event : EditorFactoryEvent ) {
// ...
editor.document.addDocumentListener( AutoDevDocumentListener (editor), editorDisposable)
editor.caretModel.addCaretListener( AutoDevCaretListener (editor), editorDisposable)
// ...
}
class AutoDevCaretListener ( val editor : Editor ) : CaretListener {
override fun caretPositionChanged ( event : CaretEvent ) {
// ...
val wasTypeOver = TypeOverHandler .getPendingTypeOverAndReset(editor)
// ...
llmInlayManager.disposeInlays(editor, InlayDisposeContext . CaretChange )
}
}
class AutoDevDocumentListener ( val editor : Editor ) : BulkAwareDocumentListener {
override fun documentChangedNonBulk ( event : DocumentEvent ) {
// ...
val llmInlayManager = LLMInlayManager .getInstance()
llmInlayManager
.editorModified(editor, changeOffset)
}
}
}
Anschließend werden je nach Eingabe unterschiedliche Abschlussergebnisse ausgelöst und die Struktur verarbeitet.
Render-Abschlusscode
Anschließend müssen wir einen Inlay Render implementieren, der von EditorCustomElementRenderer
erbt.
In Kombination mit den Schnittstellenfunktionen der IDE müssen wir die entsprechende Aktion, die entsprechende Gruppe und das entsprechende Symbol hinzufügen. Das Folgende ist ein Beispiel für eine Aktion:
<add-to-group group-id="ShowIntentionsGroup" relative-to-action="ShowIntentionActions" anchor="after"/>
Im Folgenden sind einige Aktionsgruppen von AutoDev aufgeführt:
Gruppen-ID | KI nutzt | Beschreibung |
---|---|---|
ShowIntentionsGroup | Code-Refactoring, Code-Interpretation, Code-Generierung, Code-Tests | Wird zum Anzeigen von Hinweisen im Codekontext verwendet und kann unter macOS über Alt + Enter und ⌥ + Enter aufgerufen werden. |
ConsoleEditorPopupMenu | Fehler beheben | Das in der Konsole angezeigte Menü, z. B. die Konsole der Programmausführungsstruktur. |
Vcs.MessageActionGroup | Generierung von Codeinformationen | Menü zum Schreiben von Commit-Nachrichten in VCS. |
Vcs.Log.ContextMenu | Codeüberprüfung, Codeinterpretation, Codegenerierung | Menü zum Anzeigen von Protokollen in VCS, verfügbare Funktionen: KI-Inspektion von Code, Generierung von Release-Protokollen. |
EditorPopupMenu | Alle sind akzeptabel | Über das Rechtsklick-Menü können Sie auch die entsprechende ActionGroup hinzufügen |
Beim Schreiben von ShowIntentionsGroup können wir uns auf die Implementierung von AutoDev beziehen, um die entsprechende Gruppe zu erstellen:
< group id = " AutoDevIntentionsActionGroup " class = " cc.unitmesh.devti.intentions.IntentionsActionGroup "
icon = " cc.unitmesh.devti.AutoDevIcons.AI_COPILOT " searchable = " false " >
< add-to-group group-id = " ShowIntentionsGroup " relative-to-action = " ShowIntentionActions " anchor = " after " />
</ group >
Aufgrund der Plattformstrategie von Intellij wird der Unterschied zwischen der Ausführung in einer Java-IDE (Intellij IDEA) und anderen IDEs wie der Python-IDE (Pycharm) noch größer. Wir müssen Kompatibilität basierend auf Multi-Plattform-Produkten bereitstellen. Eine detaillierte Einführung finden Sie unter: Plugin-Kompatibilität mit IntelliJ-Plattformprodukten
Erstens wird die Plug-in-Architektur weiter modularisiert, das heißt, es werden unterschiedliche Module für unterschiedliche Sprachen bereitgestellt. Das Folgende ist die modulare Architektur von AutoDev:
java/ # Java 语言插件
src/main/java/cc/unitmesh/autodev/ # Java 语言入口
src/main/resources/META-INF/plugin.xml
plugin/ # 多平台入口
src/main/resources/META-INF/plugin.xml
src/ # 即核心模块
main/resource/META-INF/core.plugin.xml
In plugin/plugin.xml
müssen wir die entsprechenden depends
und extensions
hinzufügen. Das Folgende ist ein Beispiel:
< idea-plugin package = " cc.unitmesh " xmlns : xi = " http://www.w3.org/2001/XInclude " allow-bundled-update = " true " >
< xi : include href = " /META-INF/core.xml " xpointer = " xpointer(/idea-plugin/*) " />
< content >
< module name = " cc.unitmesh.java " />
<!-- 其它模块 -->
</ content >
</ idea-plugin >
In java/plugin.xml
müssen wir die entsprechenden depends
und extensions
hinzufügen. Das Folgende ist ein Beispiel:
< idea-plugin package = " cc.unitmesh.java " >
<!-- suppress PluginXmlValidity -->
< dependencies >
< plugin id = " com.intellij.modules.java " />
< plugin id = " org.jetbrains.plugins.gradle " />
</ dependencies >
</ idea-plugin >
Anschließend lädt Intellij automatisch das entsprechende Modul, um mehrsprachige Unterstützung zu erreichen. Abhängig von den verschiedenen Sprachen, die wir voraussichtlich unterstützen, benötigen wir entsprechende plugin.xml
, wie zum Beispiel:
cc.unitmesh.javascript.xml
cc.unitmesh.rust.xml
cc.unitmesh.python.xml
cc.unitmesh.kotlin.xml
cc.unitmesh.java.xml
cc.unitmesh.go.xml
cc.unitmesh.cpp.xml
Zum Schluss implementieren Sie einfach die entsprechenden Funktionen in verschiedenen Sprachmodulen.
Um diesen Prozess zu vereinfachen, verwenden wir Unit Eval, um zu zeigen, wie zwei ähnliche Kontexte erstellt werden.
Durch statische Codeanalyse können wir die aktuelle Funktion, die aktuelle Klasse, die aktuelle Datei usw. abrufen. Finden Sie in Kombination mit der Pfadähnlichkeit den relevantesten Kontext.
private fun findRelatedCode ( container : CodeContainer ): List < CodeDataStruct > {
// 1. collects all similar data structure by imports if exists in a file tree
val byImports = container. Imports
.mapNotNull {
context.fileTree[it. Source ]?.container?. DataStructures
}
.flatten()
// 2. collects by inheritance tree for some node in the same package
val byInheritance = container. DataStructures
.map {
(it. Implements + it. Extend ).mapNotNull { i ->
context.fileTree[i]?.container?. DataStructures
}.flatten()
}
.flatten()
val related = (byImports + byInheritance).distinctBy { it. NodeName }
// 3. convert all similar data structure to uml
return related
}
class RelatedCodeStrategyBuilder ( private val context : JobContext ) : CodeStrategyBuilder {
override fun build (): List < TypedIns > {
// ...
val findRelatedCodeDs = findRelatedCode(container)
val relatedCodePath = findRelatedCodeDs.map { it. FilePath }
val jaccardSimilarity = SimilarChunker .pathLevelJaccardSimilarity(relatedCodePath, currentPath)
val relatedCode = jaccardSimilarity.mapIndexed { index, d ->
findRelatedCodeDs[index] to d
}.sortedByDescending {
it.second
}.take( 3 ).map {
it.first
}
// ...
}
}
Für den obigen Code können wir die Importinformationen des Codes als Teil des relevanten Codes verwenden. Finden Sie dann den relevanten Code anhand der Vererbungsbeziehung des Codes. Schließlich wird der engste Kontext durch Pfadähnlichkeit gefunden.
Suchen Sie zuerst und finden Sie dann den zugehörigen Code anhand der Codeähnlichkeit. Die Kernlogik wird angezeigt:
fun pathLevelJaccardSimilarity ( chunks : List < String >, text : String ): List < Double > {
// ...
}
fun tokenize ( chunk : String ): List < String > {
return chunk.split( Regex ( " [^a-zA-Z0-9] " )).filter { it.isNotBlank() }
}
fun similarityScore ( set1 : Set < String >, set2 : Set < String >): Double {
// ...
}
Einzelheiten finden Sie unter: SimilarChunker
TODO
TreeSitter ist ein von GitHub entwickeltes Framework zur Generierung effizienter benutzerdefinierter Parser. Es verwendet einen LR(1)-Parser, was bedeutet, dass jede Sprache in O(n)-Zeit statt in O(n²)-Zeit analysiert werden kann. Es verwendet außerdem eine Technik namens „Syntaxbaum-Wiederverwendung“, die es ermöglicht, Syntaxbäume zu aktualisieren, ohne die gesamte Datei neu zu analysieren.
Da TreeSitter bereits mehrsprachige Unterstützung bietet, können Sie Node.js, Rust und andere Sprachen verwenden, um entsprechende Plug-Ins zu erstellen. Weitere Informationen finden Sie unter: TreeSitter.
Abhängig von unseren Absichten gibt es verschiedene Möglichkeiten, TreeSitter zu nutzen:
Parse-Symbol
In der Code-Suchmaschine für natürliche Sprache Bloop verwenden wir TreeSitter, um Symbole zu analysieren und so eine bessere Suchqualität zu erreichen.
; ; methods
(method_declaration
name: (identifier) @hoist.definition.method)
Entscheiden Sie dann, wie es basierend auf dem Typ angezeigt werden soll:
pub static JAVA : TSLanguageConfig = TSLanguageConfig {
language_ids : & [ "Java" ] ,
file_extensions : & [ "java" ] ,
grammar : tree_sitter_java :: language ,
scope_query : MemoizedQuery :: new ( include_str ! ( "./scopes.scm" ) ) ,
hoverable_query : MemoizedQuery :: new (
r#"
[(identifier)
(type_identifier)] @hoverable
"# ,
) ,
namespaces : & [ & [
// variables
"local" ,
// functions
"method" ,
// namespacing, modules
"package" ,
"module" ,
// types
"class" ,
"enum" ,
"enumConstant" ,
"record" ,
"interface" ,
"typedef" ,
// misc.
"label" ,
] ] ,
} ;
Chunk-Code
Im Folgenden wird beschrieben, wie TreeSitter bei der Verbesserung des Code-Chunkers von LlamaIndex durch Bereinigen von Tree-Sitter-CSTs verwendet wird:
from tree_sitter import Tree
def chunker (
tree : Tree ,
source_code : bytes ,
MAX_CHARS = 512 * 3 ,
coalesce = 50 # Any chunk less than 50 characters long gets coalesced with the next chunk
) -> list [ Span ]:
# 1. Recursively form chunks based on the last post (https://docs.sweep.dev/blogs/chunking-2m-files)
def chunk_node ( node : Node ) -> list [ Span ]:
chunks : list [ Span ] = []
current_chunk : Span = Span ( node . start_byte , node . start_byte )
node_children = node . children
for child in node_children :
if child . end_byte - child . start_byte > MAX_CHARS :
chunks . append ( current_chunk )
current_chunk = Span ( child . end_byte , child . end_byte )
chunks . extend ( chunk_node ( child ))
elif child . end_byte - child . start_byte + len ( current_chunk ) > MAX_CHARS :
chunks . append ( current_chunk )
current_chunk = Span ( child . start_byte , child . end_byte )
else :
current_chunk += Span ( child . start_byte , child . end_byte )
chunks . append ( current_chunk )
return chunks
chunks = chunk_node ( tree . root_node )
# 2. Filling in the gaps
for prev , curr in zip ( chunks [: - 1 ], chunks [ 1 :]):
prev . end = curr . start
curr . start = tree . root_node . end_byte
# 3. Combining small chunks with bigger ones
new_chunks = []
current_chunk = Span ( 0 , 0 )
for chunk in chunks :
current_chunk += chunk
if non_whitespace_len ( current_chunk . extract ( source_code )) > coalesce