Keresés tartalomra
Kategóriák
Címkék
- Java
- Spring
- Python
- IoC
- Android
- DI
- Dagger
- Thymeleaf
- Markdown
- JDK11
- AOP
- Aspect
- Captcha
- I18n
- JavaSpark
- Microframework
- Testing
- JUnit
- Security
- JWT
- REST
- Database
- JPA
- Gépház
- WebFlux
- ReactiveProgramming
- Microservices
- Continuous Integration
- CircleCI
- Deployment Pipeline
- Docker
- Mocking
- LogProcessing
- PlantUML
- UML
- Modellezés
- OAuth2
- Node.js
- DevOps
- Websocket
- PyPI
- Cucumber
- Next.js
- AI
- LLM
- Langflow
A Domino-asszisztens megtanult beszélgetni a Dominoval
Folytattam a múltkori kísérletezést a lokális LLM-mel - emlékeztetőül, egy Gemma4:12b modell használatával raktam össze egy asszisztens botot, ami a Domino használatában volt hivatott a felhasználót segíteni, beleértve a képességeinek részletezését, általános használatát és konfigurációját. Alapvetően elég jó eredményeket sikerült elérni, bár a limitált hardware környezet és ebből következően hogy emiatt csak egy kisebb modellt tudtam használni, erősen behatárolta, mit várhatok el az asszisztenstől.
Arra viszont mindenképpen kíváncsi voltam, hogy tudom-e tovább fejleszteni az asszisztens képességeit, mégpedig azzal, hogy ne csak beszélgetni lehessen a Domino-ról, hanem ténylegesen végre is tudjon hajtani műveleteket a Domino által kezelt alkalmazásokon. Az egyszerűség kedvéért a Domino által támogatott alapvető életciklus-műveletek (információ-kérés, telepítés, indítás, újraindítás, leállítás) támogatása mellett döntöttem, kiegészítve a kezelt alkalmazások listázásával. Ahhoz, hogy egy LLM képes legyen külső szolgáltatásokkal kommunikálni, egy MCP szerverre van szükségünk.
Az MCP-protokoll
Az MCP-protokoll egy JSON-RPC alapú kommuikációs protokoll, ami strukturált formátumot biztosít az LLM és egy távoli szolgáltatás közti kommunikáció lebonyolítására. Az LLM egy MCP kliens segítségével fel tudja deríteni a szerver funkcióit, majd annak válasza alapján el tudja dönteni mikor, hogyan és egyáltalán melyik funkcióját kell meghívnia. A tényleges kérés-válasz formátum kiválóan dokumentálva van az MCP-protokoll dokumentációjában, így azt nem részletezném, de úgy képzeljük el a választ, hogy minden funkciónak van egy neve, rövid leírása, illetve a támogatott paraméterei és azoknak a leírása, típusa, megszorításai, valamint ugyanezek az adatok a visszatérési értékre vonatkoztatva. A címek, a leírások és minden egyéb metainformáció, bár szinte mind opcionális, nem csak egyszerű kiegészítő adatok, mivel az LLM ezek alapján tud majd az MCP hívás paramétereiről dönteni. Az első tesztek a Domino-MCP-szerverrel azt mutatták, hogy a fentebb említett metainformációk nélkül gyakran volt pontatlan a modell reakciója, így én mindenképp tudom javasolni azok pontos és aprólékos kitöltését is.
A protokoll két úgynevezett "transport" módot támogat, ezek az "Stdio" és a "Streamable HTTP" transport. Előbbi, mint a neve is mutatja, a standard ki- és bemenetet használja, lényegében az egyik futó folyamat (az LLM) hív egy önállóan futó másik alkalmazást (ez az MCP szerver), az LLM tehát szó szerint elindítja az MCP szerver folyamatot, annak a command-line argumentumaival ad át paramétereket, illetve annak a standard kimenetre írt válaszát használja. Ilyenkor nagyon oda kell arra figyelni, hogy a szerver csak az érdemi válaszát írja a standard kimenetre, mivel bármilyen egyéb log-üzenet "beszennyezné" a választ. A Streamable HTTP transport ezzel szemben HTTP szerverfolyamaton alapul, az argumentumokat egy POST kérés body szegmensében kapja, a választ pedig SSE (Server-Sent Events) formátumú válaszban biztosítja a hívó félnek (bár ezutóbbi opcionális). Mivel a Domino eleve egy REST alkalmazás, utóbbi transport mellett döntöttem.
A Domino-MCP szerver
A Domino egy TypeScript-Express alkalmazás, így az annak megfelelő MCP server implementációra volt szükségem. Ez a library viszont csak az alap szerverképességeket biztosítja, modulárisan elérhető több különböző szerveralkalmazáshoz adapter, így az Express-hez is. Mint az kiderült, ez csak az Express 5-es főverziójával kompatibilis, így ehelyett a Vercel által, elsősorban a Next.js-hez készített adaptert tudtam használni, és ahhoz is szükség volt egy rövid konverziós kódra, ami megtalálható az adapter dokumentációjában. Így gyakorlatilag egy Express middleware-t kapunk, amit POST útvonalként kell regisztrálnunk.
export function createDominoMcpHandler(): RequestHandler {
// a createMcpHandler az említett Vercel adapter libraryből származik
const mcpHandler = createMcpHandler(server => {
// ide jönnek majd a tool regisztrációk
}, {
serverInfo: {
name: "domino-lifecycle",
version: "0.0.1",
}
});
// ez itt az Express v5 -> v4 request formátum konverzió
// Express v5 esetén erre az egészre nincs szükség
return async (req: Request, res: Response) => {
const url = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
const webRequest = new Request(url, {
method: req.method,
headers: req.headers as HeadersInit,
body:
req.method !== "GET" && req.method !== "HEAD"
? JSON.stringify(req.body)
: undefined,
});
const webResponse = await mcpHandler(webRequest);
res.status(webResponse.status);
webResponse.headers.forEach((value, key) => {
res.setHeader(key, value);
});
const body = await webResponse.text();
res.send(body);
};
}
Ezután jöhettek az input/output sémák. Mint azt korábban említettem, jól-definiált sémák nélkül az LLM pontatlan válaszokat adott, nehezen tudta bizonyos paraméterekről eldönteni, hogy hogyan használja azokat. Valószínűleg ez is a kisebb modell hátránya, de ezt persze sem megerősíteni, sem megcáfolni nem tudtam. Az életciklus kérések input sémája például így nézett ki (ez konkrétan a telepítés műveleté, a többihez van még ennek két változata, amikben kevesebb paraméter van):
export const deployVersionRequestSchema = z.object({
deployment: z.string()
.describe("Deployment name (ID)"),
version: z.null().or(z.string()
.describe("Version to be deployed. Only applicable for 'deploy-version' operation.")),
roll: z.boolean()
.describe("Flag to indicate rolling all instances of the deployment. Omit for single-instance deployments. Not applicable for 'deployment-info' operation."),
instance: z.null().or(z.string()
.describe("Instance name suffix for multi-instance deployments. Omit for single-instance deployments."))
});
A fenti sémát a zod nevű validációs library segítségével lehet létrehozni. Nagyjából értelemszerűek a direktívái, de például definiálható vele opcionális mező (lehet null vagy egy konkrét érték), illetve jelen vannak a rövid leírások, amik az LLM "tájékozódásához" szükségesek. Korábban említettem, hogy ebben az esetben viszont nem csak az input sémája fontos, hanem az output sémája is, mivel az LLM a válasz tartalma alapján tud bizonyos döntéseket meghozni a további műveletek előtt. Az alábbi például a Domino által kezelt alkalmazások listájának sémája:
export const deploymentSummarySchema = z.object({
id: z.string()
.describe("Deployment ID"),
sourceType: z.enum(SourceType)
.describe("Source type of the deployment (Docker or Filesystem)"),
executionType: z.enum(DockerExecutionType).or(z.enum(FilesystemExecutionType))
.describe("Execution type of the deployment (Run command based for Docker or Executable/Service/Runtime based for Filesystem)"),
home: z.string()
.describe("Home directory (Filesystem source) or Docker image location (Docker source) of the deployment"),
resource: z.string()
.describe("Resource name of the deployment (executable file name for Filesystem source or Docker container name for Docker source)"),
multiInstance: z.boolean()
.describe("Flag indicating whether the deployment is multi-instance or not"),
instanceSuffixes: z.array(z.string())
.describe("List of configured instance name suffixes for multi-instance deployments"),
locked: z.boolean()
.describe("Flag indicating whether the deployment is locked for modification or not. Imported deployments are always locked by default."),
});
export const deploymentSummaryPageSchema = z.object({
pageNumber: z.number(),
pageSize: z.number(),
itemCountOnPage: z.number(),
totalPages: z.number(),
totalItemCount: z.number(),
items: z.array(deploymentSummarySchema),
})
Itt egy kicsit több is a "hint" az LLM számára, mint az input sémában, például hogy az "executionType" paraméter értéke függ a "sourceType" paramétertől, vagy hogy az "instanceSuffixes" csak a "multiInstance" alkalmazásokra jellemző. Mivel a legtöbb művelet hibára is futhat, szükség lesz a hibaüzenetek sémájára is, amit együtt, "vagy" művelettel kell majd használnunk az alapsémával:
export const errorSchema = z.object({
message: z.string()
.describe("Short description of the error"),
violations: z.optional(z.array(z.object({
field: z.string()
.describe("Name of the field that caused the error"),
constraint: z.string()
.describe("Name of the constraint that caused the error"),
message: z.string()
.describe("Detailed description of the constraint violation"),
})))
});
Nagyon fontos megemlíteni, hogy ha output sémát definiáltunk és a szerver nem csak plain text formátumban válaszol, hanem strukturáltan is, akkor a kimenetet validálni fogja a library a séma ellen. Itt amúgy felmerül egy érdekes kérdés is, ugyanis az MCP protokoll dokumentációja azt javasolja, hogy mindkét formátumban válaszoljon a szerver minden kérésre, ugyanis a strukturált formátum még viszonylag új direktívája a protokollnak, így a visszafele kompatibilitást biztosítja ez a lépés, ugyanakkor javasolja a strukturált formátum használatát a plain text ellenében. Kicsit fura szerintem ugyanis így minden válasz kétszer tartalmazza a visszatérési értékeket, egyszer JSON, egyszer pedig plain text formátumban (ami itt szintén JSON lesz). Ez egyébként a meglévő Domino service rétegre épített MCP adapterben így néz ki:
export class McpAdapter {
// ...
public handleRegisteredDeployments(): ToolCallback {
return async () => this.formatResponse(await this.deploymentDefinitionService.getDeploymentsPaged(0, 200));
}
public handleLifecycleOperation(lifecycleOperation: LifecycleOperation): ToolCallback<typeof infoRequestSchema> {
return async (args: LifecycleRequest) => {
// itt készül el a service rétegnek szánt kérés az MCP szerver által átadott paraméterekből
const request: DeploymentAttributes = {
deployment: args.deployment,
version: args.version || undefined,
instance: args.instance || undefined,
roll: args.roll || false
};
try {
return this.formatResponse(await this.endpointMap.get(lifecycleOperation)!(request))
} catch (error) {
// a konzisztens válaszok érdekében a hibákat is JSON formátumban adja vissza a kód
return this.formatResponse({
message: `Failed to contact Domino: ${error}`
});
}
}
}
// itt készítem el az MCP szerver által elvárt formátumú választ
private formatResponse(response: any): any {
return {
content: [
{
type: "text",
text: JSON.stringify(response)
}
],
structuredContent: response
};
}
}
Most már csak a funkciókat kell regisztrálnunk, ehhez a korábban tárgyalt createDominoMcpHandler függvényhez kell visszatérnünk:
server.registerTool("registered-deployments", {
title: "Registered Deployments",
description: "Lists the deployments currently registered in Domino",
outputSchema: deploymentSummaryPageSchema.or(errorSchema)
}, mcpAdapter.handleRegisteredDeployments());
server.registerTool("deployment-info", {
title: "Deployment Info",
description: "Provides information about a specific deployment. For multi-instance deployments, info always must be requested separately per instance.",
inputSchema: infoRequestSchema
}, mcpAdapter.handleLifecycleOperation(LifecycleOperation.INFO));
server.registerTool("deploy-version", {
title: "Deploy version",
description: "Deploys a specific version of a deployment",
inputSchema: deployVersionRequestSchema,
outputSchema: operationResultSchema.or(errorSchema)
}, mcpAdapter.handleLifecycleOperation(LifecycleOperation.DEPLOY));
server.registerTool("start-deployment", {
title: "Start deployment",
description: "Starts the currently deployed version of a deployment",
inputSchema: lifecycleRequestSchema,
outputSchema: operationResultSchema.or(errorSchema)
}, mcpAdapter.handleLifecycleOperation(LifecycleOperation.START));
server.registerTool("stop-deployment", {
title: "Stop deployment",
description: "Stops the currently deployed version of a deployment",
inputSchema: lifecycleRequestSchema,
outputSchema: operationResultSchema.or(errorSchema)
}, mcpAdapter.handleLifecycleOperation(LifecycleOperation.STOP));
server.registerTool("restart-deployment", {
title: "Restart deployment",
description: "Restarts the currently deployed version of a deployment",
inputSchema: lifecycleRequestSchema,
outputSchema: operationResultSchema.or(errorSchema)
}, mcpAdapter.handleLifecycleOperation(LifecycleOperation.RESTART));
Minden egyes regisztráció gyakorlatilag ugyanazokat a paramétereket tartalmazza: a funkció azonosítója, amit az LLM meg fog hívni, egy cím, rövid leírás (ezek a "tájékozódásban" segítik az LLM-et), az input és output sémák, illetve maga a service-adapter hívások. Ha mindent jól csináltunk és elindítjuk a szervert, már csak konfigurálnunk kell egy arra alkalmas eszközben, és már meg is lehet hívni. Mivel az IntelliJ IDEA Jetbrains AI plugin-ja támogatja az egyedi MCP szerverek regisztrálását, én amellett döntöttem a teszteléshez. Fontos megemlítenem, hogy ha a Jetbrains AI saját modelljét használjuk a kis egyedi MCP szerver hívására, meglepően gyorsan darálja le a krediteket, így óvatosan a tesztelgetéssel, később már én is inkább lokális modellre váltottam. A konfiguráció egyébként így néz ki IDEA-ban:

Aztán már meg is kérdezhetjük a modellt, hogy ismeri-e az újdonsült MCP szervert:

Persze a kísérlet célja nem az volt, hogy egy általános modellel, a Domino tudásanyaga nélkül hívjuk meg annak rögtönzött MCP szerverét, így a következő lépés a szerver bekötése volt az asszisztensbe.
Az asszisztens bővítése
A Langflow szerencsére natívan támogatja az MCP szerverek használatát, beleértve az egyedi fejlesztésűeket is persze. A konfiguráláshoz a bal menüben válasszuk az "MCP Servers" menüpontot, majd a bal alsó sarokban kattintsunk az "Add Server" gombra. Az alábbi felületet fogjuk kapni, ahol a szerver nevét és címét kell csak megadnunk (a képet már a szerver létrehozása után készítettem, ezért itt már a szerver frissítése látható, de gyakorlatilag ugyanígy néz ki a létrehozás is):

A létrehozott szervert ezután hozzá kell adni a workflow-höz. Ha mindent jól csináltunk, az MCP szerver neve melletti frissítés gombra kattintva listázni fogja az elérhető funkciókat, amiből egyet választani kell. Nyilván a cél most az lenne, hogy bármelyiket tudja majd használni az asszisztens, így újfent "Tool" módba kell tennünk a komponenst, így az alábbi állapotot kapva:

Az Agent "Tools" inputjára kötve az MCP komponenst már használhatóvá is válik. A probléma ezen a ponton az, hogy így viszont még nem fogja használni az asszisztens az MCP szervert, mivel a promptja nem hívja fel rá a figyelmét, sőt jelenleg az a kikötés is érvényes, hogy csak a Knowledge Base komponenst használhatja válaszadásra. A prompt bővítésére újfent a Gemma4 modellt használtam, a cikk végén mellékelem a kibővített promptot, lényegében csak egy új szegmenst adott hozzá ("Active Lifecycle Operations"), ami felhívja a modell figyelmét a szerverre, illetve az általa biztosított 6 funkcióira (expliciten, név szerint, rövid leírással). Érdekesség, hogy a frissített promptba a Gemma elhelyezett arra vonatkozóan is egy kitételt, hogy próbálja majd az agent megkülönböztetni a "hogyan végezhetem el ezt, hogy konfigurálom azt, ..." üzeneteket a "hajtsd végre ezt, kérd le azt, ..." jellegű kérésektől. Így az agent jól behatárolhatóan fog váltani a "két üzemmódja" között: a konfigurációs asszisztens és a Domino "üzemeltető". Az előzetes tesztjeim közben egyébként ez a váltogatás egész szépen meg is jelent, az alább mellékelt chatlogban is látható, hogy az első kérdésemre még próbálja eldönteni, hogy pontosan miben is kérem a segítségét. A teszt egyébként meglepően sikeres volt, az Agent nagyon jól reagált a kéréseimre, ha bizonytalan volt vagy hibával tért vissza az MCP hívás, akkor ajánlott javítási lehetőségeket (méghozzá helyeseket), aztán valahogy elkezdett szétesni az egész... És ezzel eljutottunk a kísérlet lezárásához és konklúziójához.
A Verdikt
A teszt a 10. üzenetem környékén kezdett látványosan szétesni (a teljes chatlogot mellékelem a cikk végén csatolmányként), ahol konkrétan hazudott (professzionális kifejezéssel hallucinált) az agent arról, hogy ő az alkalmazás leállítását elvégezte. Nyilván közben a Langflow teljes chatlogján látszott, hogy az MCP szervert nem hívta meg, illetve közben a szerver logjait is figyeltem, azon sem látszott a művelet végrehajtása. Itt még sikerült egy picit visszakanyarodni a megfelelő útvonalra, ekkor már nagyon nehezen, de még egy alkalmazás indítást ki tudtam könyörögni belőle (itt már többször is jeleznem kellett, hogy valójában nem hívta meg a Domino-t), de aztán jött az ominózus alkalmazás leállítás, aminek a válaszára majd 11 percig vártam, közben a logban figyelve, hogyan szenved az MCP hívással, folyamatosan rosszul paraméterezi azt és mindig hibát kap. Ez volt az a pont, amikor úgy döntöttem, hogy erős jóindulattal, de sikeresnek mondom a kísérletet, leállítok mindent és megírom a cikket róla.
De mi történt valójában? Nos, egyszerűen annyi, hogy megtelt az összes létező memória a gépemben. A VRAM ezen a ponton már 11.5 GB-os foglaláson állt (12-ből), a rendszermemória 29-30 GB-on (a 32-ből). Lényegében az agent "megfulladt" a rengeteg gyorsítótárazott adatban, új "gondolatmenetek" létrehozására pedig már valószínűleg nem maradt hely, legalábbis az utolsó néhány művelet teljes szétesése erre enged következtetni. Ezen a ponton talán még a modell és a Langflow újraindítása segíthet, bár azt vettem észre, hogy leginkább a teljes gépem újraindítása szokott csak javítani a helyzeten (a korábbi tesztek alatt legalábbis ez volt a helyzet). Így persze felmerül a kérdés, hogy mi értelme volt ennek az asszisztensnek, 6-8 művelet végrehajtása még közel zökkenőmentesen ment, aztán gyakorlatilag használhatatlanra redukálódott a teljesítmény. A válaszom: semmi, abszolút semmi értelme nem volt, ez egy kísérlet volt, amivel szerettem volna bizonyítani, hogy egy viszonylag régi hardware konfiguráción is képes futni egy LLM, még ha nem is hatékonyan. A Domino saját CLI alkalmazással rendelkezik, aminek minden szempontból összehasonlíthatatlanul kisebb az erőforrásigénye, lévén egy Python-ban írt alkalmazásról szó. És ami még fontosabb, minden esetben determinisztikus a működése, a CLI nem fogja azt hazudni, hogy elindította a telepítést, miközben meg sem hívta a Domino Coordinator API-t.
Már az előző cikkemben is említettem, hogy szerintem nagyon fontos jól megválasztani egy ilyen Agentic Workflow célját, és a legfontosabb kérdés mindig az kell legyen, hogy egyáltalán szükség van-e rá. Esetemben, az MCP szerverben lényegében újra megírtam a CLI néhány belépési pontját, ami így már csak időpazarlás volt, de tegyük fel, hogy a CLI még nem létezik. Ebben az esetben megírtam a szükséges kapcsolódási felületet, amitől betonstabil viselkedést várok, majd ráraktam egy bizonytalan, aránytalanul erőforrásigényes vezérlőréteget, ami alig néhány művelet után használhatatlanná vált. Így megfogalmazva már egyáltalán nem hangzik jó ötletnek.
Mindenesetre egyáltalán nem bántam meg, hogy belevágtam ebbe a kísérletbe, és mindenképpen szeretnék még később visszatérni hozzá, valószínűleg miután videókártyát cseréltem a gépemben, valami újabb és erősebb modellre. :) A kísérletet magát abszolút sikeresnek gondolom, amit szerettem volna, azt sikerült is elérnem, mellette pedig rengeteget tanultam az LLM-ekről, azok üzemeltetéséről, MCP protokollról és szerver implementációról, Agentic Workflow építésről és annak, mondhatni, optimalizációjáról. A cikkben említett minden eszköz ingyenesen elérhető, beleértve a modelleket is, így ha bárki szeretne belevágni hasonlóba, nyugodtan tehet vele egy próbát, bár 12 GB-nál kevesebb VRAM-mal személy szerint nem is próbálkoznék. Ez pedig, azt hiszem, egy tökéletes végszó, köszönöm a figyelmet.
Komment írásához jelentkezz be
Bejelentkezés

Még senki nem szólt hozzá ehhez a bejegyzéshez.