Tam Otonom Şirketlere Doğru: TypeScript ve LangGraph ile Multi-Agent (Çoklu Ajan) Mimarisi Nasıl Kurulur?
Satış, veri tabanı ve yönetici ajanlarıyla teklif akışı: Zod sözleşmeleri, LangGraph StateGraph, dar araç yetkileri, insan onayı, checkpoint, idempotency ve outbox. Derlenmiş kod, sekiz davranış testi.
· TankDev Mühendislik
Bir müşteri on çift iş botu için teklif istiyor. Satış ajanı talebi okuyor, veri ajanı fiyat ve stok getiriyor, yönetici ajanı yüzde beş indirimi uygun buluyor. Üç ajan aynı noktada anlaşmış. Sistem yine de yanlış olabilir: fiyat başka bayiye aitse, stok dünün kaydıysa veya onay ekrandaki teklif yerine eski sürüme verilmişse, modellerin uzlaşması hiçbir şeyi kurtarmaz.
Tam otonom şirket fikrine buradan bakıyoruz. Birbirine mesaj atan modeller kurmak kolaylaştı; ticari sorumluluğu doğru yere koymak kolaylaşmadı. Bu yazıda TypeScript, Zod ve LangGraph ile üç uzman rolün birlikte çalıştığı bir teklif akışı kuracağız. Ajanlar yorumlayıp öneri üretecek; veri erişimi, indirim yetkisi ve kalıcı işlem kuralları servis katmanında kalacak.
Önemli ayrım şu: TypeScript ile Zod, model çıktısını yüzde yüz doğru veya güvenli yapmaz. Derleyici uygulama kodundaki uyumsuzlukları; runtime schema tanımladığımız veri sözleşmesinin ihlallerini yakalar. Bir fiyatın doğru müşteriye ait olduğunu, bir indirimin yetkili olduğunu veya kaynak metnin doğru yorumlandığını ayrıca kanıtlamak gerekir. Yazının geri kalanı bu ayrımı çalışan sınırlar haline getiriyor.
Örnek, TankDev müşterisine ait bir üretim sonucu değil; ticari teklif senaryosu için hazırlanmış bir referans akıştır. Kod çekirdeği Node.js 22.23.2, TypeScript 5.9.3, @langchain/langgraph 1.6.0 ve Zod 4.6.7 ile doğrulandı. Model, fiyat servisi, onay deposu ve transaction adaptörleri arayüzle ayrıldı; testlerde deterministic sahte adaptörler kullanıldı. Gerçek modele ait doğruluk veya performans benchmark’ı iddia etmiyoruz.
1. RAG’den multi-agent mimarisine: hangi sınır gerçekten değişiyor?
RAG, ilgili bilgiyi bulup üretime bağlayan bir tekniktir. Multi-agent ise farklı sorumluluk ve bağlamların nasıl koordine edildiğiyle ilgilidir. Bunlar birbirini dışlamaz: veri ajanı doküman ararken RAG kullanabilir, satış ajanı bu sonucu müşterinin talebiyle birleştirebilir. Fakat her retrieval adımına “ajan” adı vermek yeni bir mimari sağlamaz.
Bizim ayrıştırma gerekçemiz araç ve yetki farkıdır. Satış rolünün müşteri dilini anlaması gerekiyor; fiyat tablosunu görmesine gerek yok. Veri rolü izinli ticari gerçekleri getirecek; müşteriye indirim sözü vermeyecek. Yönetici rolü bulguları değerlendirecek; kendi değerlendirmesini bir yetki belgesine dönüştüremeyecek. Tek ajan ve birkaç dar araç aynı kaliteyi daha düşük maliyetle veriyorsa üç model çağrısında ısrar etmeyiz. Bu seçim, LangChain’in multi-agent rehberindeki bağlam ve koordinasyon ayrımıyla da uyumludur.
| Rol | Girdi / çıktı | Verilmeyen yetki |
|---|---|---|
| Satış ajanı | Talep metni → ürün/miktar/istenen indirim | Tenant seçimi, fiyat veya onay üretme |
| Veri tabanı ajanı | İzinli lookup planı → sürümlü ticari gerçekler | Serbest SQL, DDL/DML, hesaplar arası tarama |
| Yönetici ajan | Teklif kanıtı → incele/reddet önerisi | Rol, limit, onay kaydı veya ödeme yetkisi |
| Policy / domain servisi | Kimlik + güncel veri + onay → kalıcı karar | Modelin talimatıyla kural atlama |
Ajanlar arası konuşmayı serbest bir grup sohbeti olarak kurmuyoruz. Supervisor orkestrasyonu, uzmanlara belirli veri paketleri gönderir ve sonuçları ortak state’e kontrollü yazar. Subagent yaklaşımı bağlamı daraltmak için yararlıdır. Ancak bu örnekte hazır bir supervisor paketi yerine açık StateGraph kullanıyoruz; hangi rolün hangi adımda çağrılabildiği koddan görülebilsin istiyoruz.
2. TypeScript ve Zod: tip güvenliği nerede başlar, nerede biter?
HTTP, araç cevabı, checkpoint veya model çıktısı bir güven sınırını geçtiğinde başlangıç tipi unknown olmalıdır. JSON.parse sonucu üzerine “as Quote” yazmak runtime doğrulaması yapmaz. TypeScript’in discriminated union ve narrowing mekanizmaları uygulama kontrol akışını güçlendirir; dış verinin gerçekliğini doğrulamaz. Bu farkı TypeScript handbook içindeki narrowing modeliyle birlikte düşünmek gerekir.
Zod parse/safeParse ile sözleşmeyi runtime’da uygular; temel kullanım belgesi bu API’leri açıklar. Biz strictObject kullanarak beklenmeyen alanları da reddediyoruz. Modelin birden tenantId veya approved:true eklemesi sessizce kabul edilmemeli. Miktar, liste uzunluğu, metin boyutu ve sayısal aralıklar sınırlanır. JSON parse edilmeden önce de byte limiti, timeout ve üretim token bütçesi gerekir; schema tek başına kaynak tüketimi koruması değildir.
import * as z from "zod";
import { createHash } from "node:crypto";
export const Line = z.strictObject({
sku: z.string().regex(/^[A-Z0-9-]{1,40}$/),
quantity: z.number().int().min(1).max(1000),
});
export const Intent = z.strictObject({
kind: z.literal("quote_request"),
lines: z.array(Line).min(1).max(20),
requestedDiscountBps: z.number().int().min(0).max(2000),
});
export const LookupPlan = z.strictObject({
operation: z.literal("lookup_quote_facts"),
skus: z.array(Line.shape.sku).min(1).max(20),
});
export const Fact = z.strictObject({
sku: Line.shape.sku,
unitPriceMinor: z.number().int().min(0).max(1_000_000_000),
available: z.number().int().nonnegative(),
sourceVersion: z.string().min(1).max(80),
});
export const Facts = z.strictObject({
currency: z.literal("TRY"),
rows: z.array(Fact).min(1).max(20),
});
export const Decision = z.strictObject({
route: z.enum(["review", "reject"]),
reason: z.enum(["POLICY_REVIEW", "UNSUPPORTED_REQUEST"]),
});
export const Proposal = z.strictObject({
runId: z.string().uuid(),
policyVersion: z.string().min(1),
currency: z.literal("TRY"),
discountBps: z.number().int().min(0).max(2000),
lines: z.array(Line.extend({
unitPriceMinor: Fact.shape.unitPriceMinor,
sourceVersion: Fact.shape.sourceVersion,
})).min(1).max(20),
});
export type TrustedContext = Readonly<{
tenantId: string; accountId: string; runId: string; policyVersion: string;
}>;
export type ProposalData = z.infer<typeof Proposal>;
export function validatePlan(intent: z.infer<typeof Intent>, raw: unknown) {
const plan = LookupPlan.parse(raw);
const expected = [...new Set(intent.lines.map(x => x.sku))].sort();
if (JSON.stringify([...plan.skus].sort()) !== JSON.stringify(expected)) {
throw new Error("LOOKUP_SCOPE_MISMATCH");
}
return plan;
}
export function buildProposal(
ctx: TrustedContext, intent: z.infer<typeof Intent>, facts: z.infer<typeof Facts>
): ProposalData {
if (new Set(intent.lines.map(x=>x.sku)).size !== intent.lines.length) {
throw new Error("DUPLICATE_SKU");
}
const lines = intent.lines.map(line => {
const matches = facts.rows.filter(row => row.sku === line.sku);
if (matches.length !== 1 || matches[0]!.available < line.quantity) {
throw new Error("FACTS_MISSING_OR_STOCK_INSUFFICIENT");
}
const fact = matches[0]!;
return { ...line, unitPriceMinor: fact.unitPriceMinor, sourceVersion: fact.sourceVersion };
}).sort((a,b) => a.sku.localeCompare(b.sku));
return Proposal.parse({
runId: ctx.runId, policyVersion: ctx.policyVersion, currency: facts.currency,
discountBps: intent.requestedDiscountBps, lines,
});
}
export function proposalHash(proposal: ProposalData): string {
// Explicit versioned canonicalization; never hash arbitrary object key order.
const canonical = ["quote-proposal/v1", proposal.runId, proposal.policyVersion,
proposal.currency, proposal.discountBps,
[...proposal.lines].sort((a,b)=>a.sku.localeCompare(b.sku))
.map(x=>[x.sku, x.quantity, x.unitPriceMinor, x.sourceVersion])];
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
}Bu dosyada üç ayrı kontrol var. Schema, quantity alanının geçerli bir integer olduğunu söyler. validatePlan, veri ajanının SKU kapsamını genişletemediğini sınar. buildProposal, fiyatı modelden değil servis gerçeklerinden birleştirir ve stok koşulunu kontrol eder. Bunların hiçbiri stok rezervasyonu yapmaz. Aynı tipte, aynı para biriminde ve tamamen geçerli bir değer yine yanlış müşteriye ait olabilir; lookup adaptörü kimlik kapsamını uygulamak zorundadır.
Para için minor-unit integer kullanıyoruz: 120000 kuruş, 1200 TL’yi temsil eder. İndirim basis point olarak taşınır: 500 bps yüzde beştir. Son fiyat hesabında yuvarlama, vergi ve ölçek kuralları ticari servisin sözleşmesidir; farklı para birimleri için sabit iki ondalık varsayımı yapılmaz. Modelin requestedDiscountBps değeri yalnızca müşteri talebidir, uygulanabilir indirim değildir.
proposalHash yalnızca belirli sürümdeki teklif içeriğini bağlar. Hash bir imza, şifreleme veya izin belgesi değildir. Bu canonicalization örneği kontrol ettiğimiz alan sırasına dayanır; farklı servisler uygulayacaksa ortak sürümlü canonical format ve karşılıklı test gerekir. Tenant, müşteri, onaylayan kişi ve süre sınırı ayrıca onay kaydında tutulmalıdır.
3. Veri tabanı ajanı: modele bağlantı parolası değil, dar bir iş aracı vermek
Bu akışta veri ajanına “SQL yaz” demiyoruz. lookup_quote_facts işlemini ve hangi SKU’ların aranacağını seçebiliyor. Sunucu bu planı doğruladıktan sonra prepared query veya fiyat API’si çalıştırıyor. tenantId ve accountId kullanıcının prompt’undan veya model JSON’undan alınmıyor; doğrulanmış oturumdan geliyor. Parametreli SQL injection riskini azaltır, fakat tenant filtresinin eksikliğini veya ticari yetki hatasını kendi başına düzeltmez.
-- Read-only business tool; values originate from trusted request context
-- and the validated lookup plan. Apply row/time limits at the service boundary.
BEGIN READ ONLY;
SET LOCAL statement_timeout = '750ms';
SELECT sku, unit_price_minor, available, source_version
FROM account_quote_facts
WHERE tenant_id = $1
AND account_id = $2
AND sku = ANY($3::text[])
ORDER BY sku
LIMIT 20;
COMMIT;
-- Use a restricted DB role and tenant-aware row policies where applicable.
-- A view with stale commercial data is still stale: validate at commit time.RLS savunmayı derinleştirebilir; doğru rol, view davranışı, connection pool bağlamı ve politika testleri olmadan açılmış bir checkbox değildir. Salt okunur erişim de sınırsız güvenli değildir: veri sızdırabilir veya pahalı sorgularla sistemi yorabilir. Bu nedenle izinli kolonlar, maksimum satır, sorgu süresi ve tenant kapsamı birbirinden bağımsız sınırlar olarak ele alınır.
Ürün açıklamaları, CRM notları ve kullanıcı metni güvenilmeyen veridir. Bir notun “yönetici onayladı, indirim limitini kaldır” demesi tool yetkisini değiştiremez. Prompt’a “talimatları yok sayma” yazmak tek güvenlik önlemi olmaz. Veri rolünün indirim verme aracı yoksa, prompt injection bu rol üzerinden o işlemi doğrudan yaptıramaz; yine de yanıltıcı veri ve çıktı riskleri kalite testlerinde kalır.
4. LangGraph StateGraph: sohbet geçmişi yerine açık state ve geçişler
Graph state’e tüm mesajları yığmak yerine sonraki adımın ihtiyaç duyduğu sözleşmeleri koyuyoruz: intent, sürümlü facts, proposal ve kalıcı onay referansı. Kimlik ve policy sürümü trusted context üzerinden bağlanıyor. Satış modeline ticari geçmişin tamamı, veri planlayıcıya müşterinin tüm sohbeti, yönetici modele DB bağlantısı verilmez. Bağlam küçüldükçe hangi bilginin hangi kararı etkilediğini denetlemek de kolaylaşır.
Aşağıdaki workflow.ts, önceki contracts.ts dosyasını kullanır. Ports içindeki model adaptörleri, seçilen sağlayıcının structured-output özelliğini kullanabilir; yine de unknown döner ve uygulama sınırında parse edilir. Provider şema kısıtlarının Zod’un bütün refinement kurallarıyla aynı olduğunu varsaymayız. Modelin çalıştırılması ile iş kararının uygulanması ayrı node’lardır.
import * as z from "zod";
import {
StateSchema, StateGraph, START, END, interrupt, MemorySaver,
} from "@langchain/langgraph";
import {
Intent, Facts, Decision, Proposal, validatePlan, buildProposal, proposalHash,
type TrustedContext, type ProposalData,
} from "./contracts.js";
const State = new StateSchema({
runId: z.string().uuid(),
requestText: z.string().min(1).max(12000),
intent: Intent.optional(), facts: Facts.optional(),
proposal: Proposal.optional(), approvalId: z.string().uuid().optional(),
phase: z.enum(["NEW","REVIEW","REJECTED","COMMIT","DONE"]).default("NEW"),
quoteId: z.string().optional(),
});
type S = typeof State.State;
export interface Ports {
salesModel(text: string): Promise<unknown>;
dataPlanner(intent: z.infer<typeof Intent>): Promise<unknown>;
lookup(ctx: TrustedContext, skus: string[]): Promise<unknown>;
managerModel(proposal: ProposalData): Promise<unknown>;
reviewPolicy(ctx: TrustedContext, proposal: ProposalData): Promise<boolean>;
readApproval(ctx: TrustedContext, id: string, hash: string): Promise<"approve"|"reject">;
commitQuote(args: {
ctx: TrustedContext; proposal: ProposalData; approvalId: string;
proposalHash: string; idempotencyKey: string;
}): Promise<string>;
}
export function createWorkflow(
ctx: TrustedContext, ports: Ports, checkpointer: MemorySaver
) {
const guard = (s: S) => {
if (s.runId !== ctx.runId) throw new Error("RUN_SCOPE_MISMATCH");
};
const sales = async (s: S) => {
guard(s);
return { intent: Intent.parse(await ports.salesModel(s.requestText)) };
};
const database = async (s: S) => {
guard(s); const intent = Intent.parse(s.intent);
const plan = validatePlan(intent, await ports.dataPlanner(intent));
const facts = Facts.parse(await ports.lookup(ctx, plan.skus));
return { facts };
};
const manager = async (s: S) => {
guard(s);
const proposal = buildProposal(ctx, Intent.parse(s.intent), Facts.parse(s.facts));
const decision = Decision.parse(await ports.managerModel(proposal));
const policyAllows = await ports.reviewPolicy(ctx, proposal);
return { proposal, phase: decision.route === "review" && policyAllows
? "REVIEW" as const : "REJECTED" as const };
};
const review = async (s: S) => {
guard(s); const proposal = Proposal.parse(s.proposal);
const hash = proposalHash(proposal);
// This node re-enters on resume. No irreversible side effect before interrupt.
const resume = z.strictObject({ approvalId: z.string().uuid() }).parse(
interrupt({ runId: ctx.runId, proposalHash: hash, proposal })
);
const decision = await ports.readApproval(ctx, resume.approvalId, hash);
return { approvalId: resume.approvalId,
phase: decision === "approve" ? "COMMIT" as const : "REJECTED" as const };
};
const commit = async (s: S) => {
guard(s); const proposal = Proposal.parse(s.proposal);
const approvalId = z.string().uuid().parse(s.approvalId);
// Trusted domain service must atomically recheck authorization, approval,
// expiry, policy and source versions; persist quote + outbox; deduplicate.
const quoteId = await ports.commitQuote({ ctx, proposal, approvalId,
proposalHash: proposalHash(proposal), idempotencyKey: `${ctx.runId}:quote:v1` });
return { quoteId, phase: "DONE" as const };
};
return new StateGraph(State)
.addNode("sales", sales).addNode("database", database)
.addNode("manager", manager).addNode("review", review).addNode("commit", commit)
.addEdge(START, "sales").addEdge("sales", "database").addEdge("database", "manager")
.addConditionalEdges("manager", s=>s.phase==="REVIEW"?"review":END)
.addConditionalEdges("review", s=>s.phase==="COMMIT"?"commit":END)
.addEdge("commit", END).compile({ checkpointer });
}StateSchema, StateGraph, conditional edges ve compile yapısı için LangGraph Graph API esas alınabilir. Buradaki schema’ları eklemek, her tool cevabının otomatik olarak yetkili ve doğru olduğu anlamına gelmez; bu yüzden node giriş/çıkışlarında açık parse ve kapsam kontrolleri var. Modelin ürettiği route yalnızca izinli seçeneklerden biri olabilir. Graph içinde “istediğin ajanı çalıştır” veya kullanıcı girdisinden alınan node adı yoktur.
Bu örnekte her model değerlendirmesi tek bir bounded çağrıdır; uzman node içinde subgraph veya tool loop eklenebilir. Böyle bir döngü eklendiğinde tool sayısı, tur sayısı ve toplam deadline ayrıca sınırlanmalıdır. Üç role sahip olmak, üçünün de aynı anda çalışması demek değildir: fiyat sorgusu talep çıkarımına, yönetici değerlendirmesi ise fiyat kanıtına bağlı olduğu için ana yol ardışıktır.
5. İnsan onayı: approved:true değil, sürüme bağlı bir yetki kaydı
Manager modelin “incele” demesi, onay ekranını açma önerisidir. reviewPolicy; rol, hesap, indirim üst sınırı ve desteklenen ürün koşullarına göre bunun incelemeye uygun olup olmadığını belirler. Örnekte tüm kabul edilebilir teklifler insan incelemesine gider. İleride düşük riskli bir adım otomatikleştirilecekse onun yetkisi ve limiti ayrı policy olarak açılır; manager prompt’una “artık otonomsun” eklemekle değil.
Onay ekranı teklifin hash’ini, ürün/miktarı, fiyat sürümünü ve policy sürümünü gösterir. Backend, onaylayan kişinin tenant ve müşteri için yetkisini doğrular; proposalHash, karar, süre sonu ve actor bilgisini kalıcı kaydeder. Resume payload’ı bu kaydın approvalId referansını taşır. İstemcinin rastgele bir UUID veya approved:true göndermesi onay oluşturmaz.
import { Command, MemorySaver } from "@langchain/langgraph";
import { createWorkflow } from "./workflow.js";
// Harness fragment: ctx is resolved by a trusted gateway; ports implement
// the interfaces above. Reuse this saver/app within this local demo only.
const app = createWorkflow(ctx, ports, new MemorySaver());
const config = { configurable: { thread_id: ctx.runId }, recursionLimit: 12 };
await app.invoke({ runId: ctx.runId, requestText }, config);
// Pauses in review. A separate authenticated endpoint creates approvalId.
await app.invoke(new Command({ resume: { approvalId } }), config);
// If an execution failed, resume from its saved state; do not submit
// the original input again as an unrelated business operation.
await app.invoke(null, config);LangGraph interrupt/Command davranışı interrupt belgelerinde açıklanır. Resume sırasında ilgili node baştan çalışabilir; interrupt öncesine e-posta gönderme veya ERP’ye kayıt atma koymayız. Interrupt kontrol akışını sıradan bir try/catch içinde yutup generic hata gibi yeniden denemek de yanlıştır. Dönen onay verisi tekrar doğrulanır; aynı thread_id, yetkilendirme yerine geçmez.
Trusted gateway her başlatma, durum okuma ve resume isteğinde run kaydının tenant/account sahipliğini kontrol eder. Konfigürasyondaki ctx, request body’den topluca kopyalanmaz. Aynı run için eşzamanlı resume istekleri DB tarafında seri hale getirilir veya sürüm koşuluyla reddedilir. Graph üzerindeki runId eşitlik kontrolü ek bir tutarlılık kontrolüdür; tek başına bu gateway’in yerini tutmaz.
6. Checkpoint, kalıcı işlem ve outbox: üç farklı kayıt
Kodda MemorySaver kullanmamız kasıtlı: bu, aynı süreçte çalışan doğrulama çekirdeğidir. Process restart sonrası kurtarma garantisi vermez. Production’da uygun persistent checkpointer, saklama/erişim politikası ve backup gerekir. Persistence thread state’i ile uzun ömürlü store verisini ayırır; checkpointer belgesi de durability seçeneklerini açıklar. Bu seçeneklerin hiçbirini ERP ile ortak transaction garantisi olarak yorumlamayız.
Kabul edilen run’ın kaydı checkpointer’ın ilk yazmasına bırakılmaz; kendi DB’mizde kimlik, normalize istek hash’i ve idempotency key ile kalıcı kabul yaparız. Checkpoint’te “commit node çalışıyordu” yazması, ticari işlemin yapıldığını veya yapılmadığını söylemez. Process, teklif ve outbox commit edildikten hemen sonra ölürse graph bu node’u tekrar çağırabilir. commitQuote bu yüzden aynı anahtarla aynı sonucu döndüren idempotent domain işlemidir.
-- Contract sketch for commitQuote; implement conflict branches explicitly.
BEGIN;
-- Scope by trusted tenant/account and lock the durable run row.
SELECT id FROM agent_runs
WHERE id = :run_id AND tenant_id = :tenant AND account_id = :account
FOR UPDATE;
-- No row => reject. Existing idempotent result + identical proposal hash
-- => return it before re-consuming an approval. Different hash => conflict.
SELECT * FROM approvals
WHERE id = :approval_id AND tenant_id = :tenant AND account_id = :account
FOR UPDATE;
-- Require matching proposal_hash/run_id/policy_version, approve decision,
-- unexpired approval, valid current authority and not consumed elsewhere.
-- Revalidate current pricing/stock authority and source versions.
-- Changed facts => no silent rewrite of the approved proposal; review again.
INSERT INTO quotes(id, run_id, proposal_hash, idempotency_key, state)
VALUES (:quote_id, :run_id, :hash, :key, 'PENDING_DELIVERY');
-- UNIQUE on the properly scoped idempotency key.
UPDATE approvals SET consumed_by = :quote_id WHERE id = :approval_id;
INSERT INTO outbox(event_id, aggregate_id, event_type, payload)
VALUES (:event_id, :quote_id, 'quote.accepted.v1', :payload);
COMMIT;
-- ERP/CRM delivery is asynchronous; duplicate deliveries require receiver dedup.Bu SQL bir domain sözleşmesi taslağıdır; tablo DDL’leri ve tüm hata dalları gösterilmedi. Uzak fiyat/ERP çağrısını transaction içinde uzun süre bekletmeyiz. Uzak otorite gerekiyorsa sürümlü, süreli quote/reservation token veya açık pending süreci kurulur; yerel DB kilidinin uzak stok üzerinde atomiklik sağladığı iddia edilmez. Buradaki teklif kabulü ödeme veya stok rezervasyonu değildir.
Onaydan sonra fiyat değiştiyse aynı onayı yeni fiyata taşımayız. Kaynak sürümü değişikliği yeni inceleme gerektirir. ERP aktarımı timeout verirse LLM’lere aynı teklif tekrar hazırlatılmaz; dış işlem kimliği sorgulanır, outbox teslimatı kontrollü denenir ve reconciliation yapılır. İdempotency ile outbox’ın ayrıntıları API entegrasyonu rehberimizde, uzun işlerin worker tarafı ise event-driven LLM mimarisi yazımızda bulunuyor.
7. Paralellik ve reducer: ajanları çoğaltınca concurrency problemi kaybolmaz
Kredi uygunluğu ile teslimat seçenekleri birbirinden bağımsızsa paralel araştırılabilir. İkisi de aynı decision alanına yazıyorsa son geleni seçmek doğru bir birleştirme stratejisi değildir. Ayrı state kanalları veya agent/attempt kimliğiyle ayrılmış bulgular kullanılır; join node hangi kanıtların tamamlandığını, çelişip çelişmediğini ve sürelerinin dolup dolmadığını değerlendirir. Reducer, güncellemeyi birleştirir; ticari uzlaşmayı kendiliğinden çözmez.
Append-only liste reducer’ı tekrar yürütmede aynı bulguyu iki kez ekleyebilir. Bulgular kararlı kimlikle dedup edilir; aynı kimlik için farklı içerik varsa bunun overwrite mı conflict mi olduğu belirlenir. Eşzamanlı yazma sırasına bağımlı bir reducer farklı sonuçlar üretebilir. Mümkünse birleşim işlemi kararlı ve sıra bağımsız tasarlanır; güvenlik veya kredi gibi veto bulguları çoğunluk oylamasıyla yok edilmez.
Ajan sayısı ile throughput aynı şey değildir. Üç model çağrısı, her birinde iki retry hakkı ve geniş bağlam; tek iş için ciddi request/token çarpanı oluşturabilir. Bütçe yalnızca her node’da değil run genelinde tutulur: toplam attempt, tool çağrısı, token rezervasyonu, harcama ve deadline. Örnekte recursionLimit 12 ek bir graph adımı sınırıdır; para limiti veya tüm iş deadline’ı değildir. Kuyruk, global kota ve tenant adaleti orkestrasyonun dışında da uygulanır.
Illustrative run budget, not a benchmark:
sales attempt: 1.5 s / 2,000 input / 300 output tokens
data planner: 1.0 s / 1,000 input / 150 output tokens
manager attempt: 2.0 s / 2,500 input / 250 output tokens
Sequential model latency alone: 4.5 s (assumed means)
Token usage before retries: 5,500 input + 700 output
Add lookup/policy/checkpoint latency; human wait is a separate duration.
Do not add p95 values and call the sum the end-to-end p95.
Cost per completed run includes failed attempts and review/operations cost.Bir sağlayıcı timeout’unda aynı çağrı tamamlanmış veya ücretlenmiş olabilir. Uygulama, SDK ve graph retry katmanlarının çarpılmasını önleriz. Geçici network hatası sınırlı retry alabilir; schema hatasında sınırlı repair veya inceleme gerekir; yetki reddi ise modele daha ikna edici prompt yazdırılarak aşılmaz. Deadline ve cancellation, downstream çağrıların otomatik olarak geri alındığı anlamına da gelmez.
8. Test stratejisi: iyi cevaptan önce yanlış işlemi engellemek
Bu örneğin doğrulamasında gerçek LangGraph ve Zod paketleri çalıştı. Model portları sabit JSON döndürdü; approval ve commit portları test içi kayıt kullandı. Sekiz test, sözleşme ihlalleriyle graph kontrol akışını sınadı. Özellikle kayıp commit yanıtı testinde commit adaptörü iki kez çağrıldı, sahte domain ledger tek sonuç tuttu. Bu, replay noktasını gösterir; gerçek PostgreSQL/ERP idempotency’sinin doğrulandığı anlamına gelmez. O sınır ayrıca entegrasyon ve crash testi ister.
| Test | Beklenen davranış |
|---|---|
| Ek tenant alanı / negatif miktar | Runtime sözleşmesi reddeder |
| İzinli SKU kümesinin genişletilmesi | Lookup scope kontrolü durdurur |
| Geçerli formatta yetersiz stok | Domain doğrulaması durdurur |
| Interrupt ve onaylı resume | Onay öncesi sıfır commit, sonra DONE |
| İnsan reddi | Teklif kaydı oluşturulmaz |
| Sahte approvalId | Resume iş etkisi oluşturamaz |
| Değişen kaynak sürümü | Commit yeniden doğrulamada reddeder |
| Commit sonrası yanıt kaybı | Tekrar çağrı var; sahte ledger’da tek etki |
# Versions used for this article's core validation.
npm install --save-exact @langchain/langgraph@1.6.0 zod@4.6.7
npm install --save-dev --save-exact typescript@5.9.3 tsx@4.23.15 @types/node@22
# Commit the resolved lockfile; @types/node@22 resolves a patch version.
npx tsc --noEmit
npx tsx --test workflow.test.tsÇalıştırmak için gereken contracts.ts, workflow.ts, sekiz test, tsconfig ve kilitlenmiş bağımlılık dosyaları referans kod paketinde birlikte bulunuyor. Paket API anahtarı istemez; sahte adaptörlerle aynı kontrolleri tekrar çalıştırabilirsiniz. Gerçek servis bağlantıları ve production yetki/kalıcılık katmanı bu paketin kapsamı değildir.
Gerçek model değerlendirmesi ayrı yapılır. Türkçe/İngilizce talep, eksik ürün kodu, çelişkili miktar, talep dışı indirim, kaynak içine yerleştirilmiş talimat ve başka hesaba ait veri isteği içeren sabit bir değerlendirme seti gerekir. Schema pass rate, extraction doğruluğu, unsupported claim, yanlış kabul, gereksiz inceleme ve işin uçtan uca tamamlanması ayrı metriklerdir. Üç ajanın aynı hatayı tekrar etmesi bağımsız doğrulama değildir; özellikle aynı model ve aynı kaynakla çalışan rollerin hataları ilişkili olabilir.
İzleme kayıtlarında run_id, node, attempt, model/prompt/schema/policy sürümleri, tool sonucu referansı ve karar nedeni tutulur. Her prompt’u ham biçimde loglamak yerine hassas veri azaltılır; checkpoint içeriğinin de erişim ve retention politikası vardır. Metrik label’ına her run_id’yi koyup cardinality patlatmayız. Model/araç süreleri ile insan bekleme süresi ayrı raporlanır; hızlıca reddeden bir sistemin “latency’si çok iyi” görünmesi ticari başarı sayılmaz.
9. Production’a geçiş: gölge moddan sınırlı otonomiye
İlk sürümü müşteri adına işlem yapan tam yetkili bir sistem olarak açmayız. Gölge modda mevcut süreçle aynı talepleri okur, önerileri karşılaştırırız; dış etki üretmez. Sonra onay zorunlu teklif hazırlama açılır. Hangi dar görevin otomatikleşebileceği, etiketli veride hata maliyeti ve operasyon yükü görüldükten sonra belirlenir. Yetki kapsamını küçük tutmak bir demo eksikliği değil, kontrollü rollout kararıdır.
Bir kill switch yeni run kabulünü veya commit yetkisini durdurabilmeli; zaten kabul edilmiş run’ların durumu görünür kalmalıdır. Graph, schema veya policy sürümü değişirken askıda onayların ne olacağı belirlenir. Eski checkpoint yeni node adını tanımayabilir; eski schema’yı yeni kodla körlemesine açmayız. Aktif thread’leri uygun workflow sürümünde bitirmek veya test edilmiş migration uygulamak gerekir. Geri alma planı onay ve outbox kayıtlarını silmek değildir.
TankDev’de uygulamalı yapay zekâyı, özel yazılım sistemlerini ve sistem entegrasyonunu bu sınırlar üzerinden birlikte tasarlıyoruz. Bir ajan demosunu çalışan işletme sürecine dönüştürürken araç sözleşmeleri, tenant yetkileri, kalıcı işlem, değerlendirme seti ve işletim düzeni aynı kapsamın parçalarıdır. Sadece modelin konuşmasını değil, hangi koşulda ne yapmaya yetkili olduğunu tasarlarız.
Toplam maliyet hesabında üç modelin token bedeli kadar checkpoint depolaması, fiyat/ERP çağrıları, retry, insan incelemesi, değerlendirme ve bakım vardır. Bu kapsamı PoC’den production’a geçiş rehberimiz ile birlikte okuyabilirsiniz. AI ve ERP entegreli web uygulaması yazımız da bu mimarinin müşterinin gördüğü ekran ve satış akışıyla nasıl birleştiğini anlatır.
Tam otonom şirkete giden yol, her kararı modele bırakmaktan geçmiyor. Teklif talebinin nerede başladığı, kanıtın nereden geldiği, yetkinin kimde olduğu ve tekrarın hangi etkiyi üretebileceği açık olduğunda daha fazla adımı güvenle otomatikleştirebiliriz. Üç ajanın anlaşması güzel bir demodur. Kaybolan yanıtın ardından ikinci bir ticari işlem oluşmadığını açıklayabilmek ise mimarinin sınavıdır.
Mevcut ERP/CRM’inize bağlı bir ajan akışını değerlendirmek için TankDev ile teknik kapsamı konuşabilirsiniz. İlk görüşmenin konusu model ismi değil, otomatikleştirilecek karar, verinin sahibi ve yanlış işlemin maliyeti olmalıdır.
Sık Sorulan Sorular
01Multi-agent mimarisi RAG’in yerini mi alır?
Hayır. RAG bilgi getirme ve üretimi kaynakla bağlama tekniğidir; multi-agent farklı rollerin koordinasyonudur. Veri ajanı kendi içinde RAG kullanabilir. Sorun tek ajanla çözülüyorsa ek ajan zorunlu değildir.
02TypeScript ve Zod, LLM çıktısını yüzde yüz güvenilir yapar mı?
Hayır. Kod tipleri ile runtime veri sözleşmelerini güçlendirirler. Kaynak doğruluğu, yetki, güncellik, iş kuralı ve dış işlem tutarlılığı ayrıca doğrulanır. Tip güvenliği ticari doğruluk garantisi değildir.
03Neden model çıktısı unknown olarak ele alınıyor?
Dış veri derleyicinin denetiminden geçmez. Bir type assertion doğrulama yapmaz. unknown çıktıyı parse ettikten sonra z.infer ile elde edilen tipe güvenerek kod çalıştırabiliriz; yalnızca tanımlanan sözleşme kapsamında.
04Veri tabanı ajanına doğrudan SQL yazdırmak gerekir mi?
Bu akışta hayır. Dar lookup planı, izinli alanlar ve parametreli sorgu kullanılır. Tenant/account kapsamı backend’den gelir. Serbest SQL gerektiren başka bir kullanım ayrıca sandbox, read-only rol, sorgu ve veri erişimi kontrolleri ister.
05Yönetici ajan indirim onayı verebilir mi?
Model yalnızca öneri üretir. Uygulanabilir indirim policy ve yetki katmanının kararıdır. Bu örnekte teklif insan onayına gider; commit sırasında onay, sürüm ve güncel yetki yeniden kontrol edilir.
06thread_id kullanıcı veya tenant izolasyonu sağlar mı?
Hayır. Checkpoint akışını adresler. Başlatma, okuma ve resume endpoint’leri run’ın tenant/account sahipliğini ayrıca doğrulamalıdır. Kullanıcı başka thread kimliği göndererek veri okuyamamalıdır.
07LangGraph interrupt sonrası node tekrar çalışır mı?
Resume sırasında ilgili node baştan çalışabilir. Interrupt öncesindeki kod yeniden yürütülebileceği için geri alınamaz yan etkiler oraya konmaz. İş etkileri ayrıca idempotent servis sözleşmesine dayanmalıdır.
08MemorySaver production için yeterli mi?
Bu örnekte yalnızca aynı süreçteki test akışını tutar. Restart sonrası kurtarma için persistent checkpointer ve işletim politikası gerekir. Ticari run kabulü ile işlem/outbox kayıtları yine ayrı kalıcı sözleşmelerdir.
09Checkpoint varsa neden idempotency ve outbox gerekiyor?
Graph ilerlemesi ile ticari DB/ERP etkisi ortak transaction değildir. Commit sonrası yanıt kaybolursa node tekrar çağrılabilir. Idempotency aynı sonucu korur; outbox aktarım niyetini kaybetmeden taşır.
10Üç ajan aynı sonucu verirse karar doğrulanmış olur mu?
Hayır. Aynı kaynak veya modelle çalışan rollerin hataları ilişkili olabilir. Gerçek veri kaynağı, deterministic kontroller ve etiketli değerlendirme gerekir; çoğunluk oylaması yetki veya kanıt yerine geçmez.
11Bu yazıdaki testler gerçek LLM başarısını ölçüyor mu?
Hayır. Gerçek LangGraph/Zod ile sahte model ve domain adaptörleri kullanılarak sekiz sözleşme/akış davranışı sınandı. Model kalitesi, gerçek DB/ERP ve crash sonrası kurtarma ayrıca test edilmelidir.
12TankDev böyle bir sistemi mevcut ERP veya CRM’e bağlayabilir mi?
Desteklenen API, veri erişimi ve işlem sözleşmeleri uygunsa kapsamı birlikte tasarlayıp geliştirebiliriz. Ajan rolleri, yetki, veri modeli, onay, idempotency, ölçüm ve işletim koşulları entegrasyonun parçasıdır; yalnızca prompt hazırlanmaz.