Wskazówki praktyczne: Efektywny pod względem pamięci agent AI: tworzenie silnika kontekstowego w Go
Praktyczne wskazówki: Efektywny pod względem pamięci agent AI: tworzenie silnika kontekstowego w Go – umowy, sprawdzania oraz gotowe elementy kodu dostępne dla zespołów wdrażających ten wzorzec.
Źródła: The Memory-Efficient AI Agent: Building a Context Engine in Go
Poniższe notatki przedstawiają praktyczną ścieżkę nauki „The Memory-Efficient AI Agent: Building a Context Engine in Go”. Nacisk kładziony jest na umowy, sprawdzania oraz miejsca zastępcze dla kodu, a nie na motywacyjne aspekty. Podczas przechodzenia przez etap przeglądu najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Wolno preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok się nie powiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany proces.
Pierwsze kroki: Importy i model danych
Proces wprowadzania w temat, importów i pierwszej fazy realizacji działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię do analizy. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres pracy. Traktuj tę fazę jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadań. Przydziel budżet tokenów na każdy ruch i sesję. Narzędzia typu agentic intensywnie rozszerzają kontekst; sztywne limity zapobiegają temu, by demonstracje przerodziły się w niespodziewane rachunki.
import (
"container/heap"
"container/list"
"encoding/gob"
"fmt"
"math"
"os"
"strings"
)
type MemoryItem struct {
ID int
Content string
Priority int
Vector []float64
index int
}
Motor priorytetów: Max-Heap z użyciem container/heap
Faza Max-Heap w silniku Priority Engine funkcjonuje najlepiej, gdy traktowana jest jako mierzalna powierzchnia. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres badania. Zapisuj czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy ścieżka przechodzi z środowiska demonstracyjnego do współdzielonych środowisk. Utrzymuj stan grafu w formie prostych, spójnych struktur. Wkładki nawiasowe ukrywają informację o tym, który węzeł zapisał dane do którego pola, co powoduje przerwanie kontynuacji po przerwach.
type PriorityQueue []*MemoryItem
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
return pq[i].Priority > pq[j].Priority // Max-heap logic
}
func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}
func (pq *PriorityQueue) Push(x interface{}) {
n := len(*pq)
item := x.(*MemoryItem)
item.index = n
*pq = append(*pq, item)
}
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
old[n-1] = nil
item.index = -1
*pq = old[0 : n-1]
return item
}
Suwak czasowy: kontekst temporalny z użyciem container/list
Faza Sliding Window Temporal funkcjonuje najlepiej, gdy jest traktowana jako mierzalna powierzchnia. Zapisz jeden idealny zapis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całego grafu. Utrzymuj stan grafu w prostej formie i z określonym typem danych. Wplecione struktury ukrywają informację o tym, który węzeł zapisał dane do którego pola, co utrudnia kontynuację pracy po przerwach. Faza Sliding Window Temporal funkcjonuje najlepiej, gdy jest traktowana jako mierzalna powierzchnia. Zapisz jeden idealny zapis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres. Wolij małe, testowalne jednostki nad rozbudowane skrypty. Gdy jakiś krok zawiedzie, awaria powinna wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji.
type SlidingWindow struct {
queue *list.List
capacity int
}
func NewSlidingWindow(capacity int) *SlidingWindow {
return &SlidingWindow{queue: list.New(), capacity: capacity}
}
func (sw *SlidingWindow) Add(item *MemoryItem) {
if sw.queue.Len() >= sw.capacity {
sw.queue.Remove(sw.queue.Front())
}
sw.queue.PushBack(item)
}
Wektorowanie i podobieństwo: wyszukiwanie semantyczne za pomocą math i strings
W fazie wektorowania i podobieństwa semantycznego należy zdefiniować dane wejściowe, osobę odpowiedzialną za daną krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Traktuj tę fazę jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy artefaktom, zdefiniuj kryteria sukcesu i odrzuć ciche, częściowe ukończenie zadania. Wymagaj ludzkiej aprobaty w przypadku operacji, które wiążą się z wydatkami lub zmianą danych produkcyjnych. Połączenia realizowane w czasie kompilacji nie równają się pełności biznesowej.
func TextToVector(text string) []float64 {
vocab := []string{"database", "connection", "fix", "leak", "system", "user"}
vec := make([]float64, len(vocab))
words := strings.Fields(strings.ToLower(text))
for _, word := range words {
for i, v := range vocab {
if strings.Contains(word, v) {
vec[i]++
}
}
}
return vec
}
func CosineSimilarity(a, b []float64) float64 {
var dot, normA, normB float64
for i := range a {
dot += a[i] * b[i]
normA += a[i] * a[i]
normB += b[i] * b[i]
}
if normA == 0 || normB == 0 {
return 0
}
return dot / (math.Sqrt(normA) * math.Sqrt(normB))
}
Persystencja: serializacja binarna za pomocą encoding/gob
Dla serializacji binarnej Persistence z etapami należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany etap oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić dany etap na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy rejestrować czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy ścieżka przechodzi z środowiska demonstracyjnego do współdzielonych środowisk. Konieczne jest ludzkie zatwierdzenie dla operacji, które generują wydatki lub zmieniają dane produkcyjne. Połączenia realizowane w czasie kompilacji nie gwarantują pełnej kompletności biznesowej.
type AgentMemory struct {
LongTerm []*MemoryItem
}
func SaveMemory(filename string, mem *AgentMemory) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
return gob.NewEncoder(file).Encode(mem)
}
Egzekucja: main()
Dla głównej fazy The Execution należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić dany krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Konfigurację należy przechowywać poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Wprowadź ludzką aprobatę dla operacji, które wiążą się z wydawaniem pieniędzy lub modyfikacją danych produkcyjnych. Połączenia skompilowane w czasie kompilacji nie gwarantują pełnej kompletności biznesowej. Dla głównej fazy The Execution należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić dany krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy preferować małe, testowalne jednostki nad rozbudowanymi skryptami. Gdy dany krok zawiedzie, powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowaną ścieżkę przetwarzania.
pq := &PriorityQueue{}
heap.Init(pq)
sw := NewSlidingWindow(3)
rawItems := []struct {
content string
priority int
}{
{"System: You are a helpful assistant.", 10},
{"User: How do I fix the database connection?", 5},
{"CRITICAL: Fix database connection leak.", 9},
}
for _, ri := range rawItems {
item := &MemoryItem{
Content: ri.content,
Priority: ri.priority,
Vector: TextToVector(ri.content),
}
heap.Push(pq, item)
sw.Add(item)
}
fmt.Println("--- Sliding Window (capacity 3) ---")
for e := sw.queue.Front(); e != nil; e = e.Next() {
fmt.Printf(" Window holds: '%s'\n", e.Value.(*MemoryItem).Content)
}
for i := 0; i < 2; i++ {
sw.Add(&MemoryItem{
ID: i + 1,
Content: fmt.Sprintf("Stream item %d: recent event", i+1),
Priority: 1,
Vector: TextToVector("recent event"),
})
}
fmt.Println(" After 2 more stream items added:")
for e := sw.queue.Front(); e != nil; e = e.Next() {
fmt.Printf(" Window holds: '%s'\n", e.Value.(*MemoryItem).Content)
}
query := "database connection fix"
queryVec := TextToVector(query)
var bestMatch *MemoryItem
maxSim := -1.0
fmt.Printf("Querying for: '%s'\n", query)
for _, item := range *pq {
sim := CosineSimilarity(item.Vector, queryVec)
fmt.Printf("Comparing with: '%s' (Sim: %.2f)\n", item.Content, sim)
if sim > maxSim {
maxSim = sim
bestMatch = item
}
}
fmt.Printf("\nBest match found: '%s' (Sim: %.2f)\n", bestMatch.Content, maxSim)
mem := &AgentMemory{LongTerm: *pq}
SaveMemory("agent_memory.gob", mem)
fmt.Println("Memory state saved to agent_memory.gob")
PS C:\Users\User\ai-memory> go run .\ai_memory_experiment.go
--- Sliding Window (capacity 3) ---
Window holds: 'System: You are a helpful assistant.'
Window holds: 'User: How do I fix the database connection?'
Window holds: 'CRITICAL: Fix database connection leak.'
After 2 more stream items added:
Window holds: 'CRITICAL: Fix database connection leak.'
Window holds: 'Stream item 1: recent event'
Window holds: 'Stream item 2: recent event'
Querying for: 'database connection fix'
Comparing with: 'System: You are a helpful assistant.' (Sim: 0.00)
Comparing with: 'User: How do I fix the database connection?' (Sim: 0.87)
Comparing with: 'CRITICAL: Fix database connection leak.' (Sim: 0.87)
Best match found: 'User: How do I fix the database connection?' (Sim: 0.87)
Memory state saved to agent_memory.gob
PS C:\Users\User\ai-memory>
Agenty przyspieszające z MCP
Podczas pracy nad etapem agentów przyspieszających z MCP najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i unikaj cichego ukończenia zadania w sposób niepełny. Zapisuj nazwę narzędzia, hash argumentów, czas reakcji oraz wynik każdego wywołania. Bez takich informacji debugowanie pętli agenta trwa godzinami.
Główne wnioski
Gdy przechodzisz przez etap kluczowych wniosków, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zapisz czas trwania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy ścieżka przechodzi z wersji demonstracyjnej do środowisk współdzielonych. Ustal punkty kontrolne po kosztownych krokach. System powrotu nie powinien ponownie naliczać opłat za tę samą wywołanie LLM, gdy operator próbuje ponownie uruchomić późniejszy węzeł.
Dodatkowe linki i zasoby
Gdy przechodzisz przez etap Zasobów Dodatkowych Linków, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Trzymaj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Ustaw kontrolne punkty po kosztownych krokach. System powinien unikać ponownego naliczania opłat za tę samą operację LLM, gdy operator próbuje ponownie uruchomić późniejszy element. Gdy przechodzisz przez etap Zasobów Dodatkowych Linków, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Wolij małe, testowalne jednostki nad rozbudowane skrypty. Gdy jakiś krok się nie powiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowaną sekwencję działań.
W fazie listy kontrolnej operacyjnej należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie wykonać dany krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu.
Zdokumentuj razem ścieżkę prawidłowego działania oraz ścieżkę naprawczą. Próby ponownych działań, kontrolne punkty ludzkie oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dodawane później.
Zapewnij ludzką zatwierdzenie w przypadkach, gdy dochodzi do wydawania pieniędzy lub zmiany danych produkcyjnych. Połączenia skompilowane w czasie kompilacji nie równają się kompletności rozwiązania biznesowego.
Napisz krótki podręcznik obsługi: jak rotować klucze, jak opróżnić kolej z zadań, jak cofnąć ostatnie operacje importu.
Niech lepsze będą małe, testowalne jednostki niż rozbudowane skrypty. Gdy dany krok zawiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji.
Należy wprowadzić ludzką kontrolę w przypadku operacji, które wiążą się z wydawaniem pieniędzy lub modyfikacją danych produkcyjnych. Konfiguracja w czasie kompilacji nie gwarantuje pełnej kompletności rozwiązania biznesowego.
Zanim wdrożymy całą architekturę, należy zamrozić dostępne wersje, utworzyć dokumentację stanu systemu dla kluczowych ścieżek przetwarzania oraz potwierdzić kroki konieczne do cofnięcia zmian. Środowiska współdzielone wymagają ograniczeń szybkości działania, weryfikacji uprawnień użytkowników oraz wyraźnego odpowiedzialnego za rotację haseł. Lepiej wybrać prostą niezawodność niż pomysłowe, jednorazowe demonstracje.
Uwaga dotycząca wersji d4b7557c44d8: unikaj przechowywania kluczy dostawcy w repozytorium, ustaw ograniczenie liczby tokenów na sesję oraz przechowuj dokumentację obok plików testowych, aby późniejsze zmiany modeli pozostawały porównywalne.
Dla etapu 0 notatki dotyczącej wzmocnienia bezpieczeństwa należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Konfigurację należy przechowywać poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić, nie musząc czytać całej struktury.
Szczegóły wzmocnienia bezpieczeństwa 0/825: należy zmierzyć czas wykonywania, klasę błędów oraz zużycie tokenów dla tej notatki, a następnie zdecydować o utrzymaniu zmiany na podstawie ustalonego zestawu pytań, a nie opisów przypadków.
Gdy przechodzisz przez pierwszy etap notatki dotyczącej wzmocnienia bezpieczeństwa, najpierw zapisz warunki kontraktu: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego awarii. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Wolno preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok zawiedzie, awaria powinna wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji.
Szczegół wzmocnienia bezpieczeństwa 1/825: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie osobistych obserwacji.
Drugi etap notatki dotyczącej wzmocnienia bezpieczeństwa działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię do analizy. Zapisz jeden idealny przypadek działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres prac. Zapisuj czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega nieoczekiwanym rachunkom, gdy przechodzi się od środowiska demonstracyjnego do wspólnych środowisk.
Szczegóły wzmocnienia 2/825: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie anegdoty.
W trzecim etapie notatki dotyczącej wzmocnienia określ dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie wykonać ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Zdokumentuj zarówno prawidłowy przebieg działania, jak i ścieżkę naprawczą. Próby ponowne, kontrola przez ludzi oraz obsługa wiadomości błędnych stanowią część produktu, a nie element późniejszej dopracowywania.
Szczegóły wzmocnienia 3/825: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie anegdoty.
Literatura pokrewna
- Praktyczne notatki: Większe okno contextu nie poprawi twojego agenta — Szczegółowy przewodnik po Praktycznych notatkach: Większe okno contextu nie poprawi twojego agenta: kontrakty, sprawdzenia oraz miejsca na kod do wstawienia dla zespołów wdrażających ten wzorzec.
- Praktyczne notatki: Dlaczego twój agent potrzebuje pamięci i jak ją zorganizować — Szczegółowy przewodnik po Praktycznych notatkach: Dlaczego twój agent potrzebuje pamięci i jak ją zorganizować: kontrakty, sprawdzenia oraz miejsca na kod do wstawienia dla zespołów wdrażających ten wzorzec.