- AG-UI Middleware๋ AbstractAgent ์คํ๊ณผ ์ด๋ฒคํธ ์๋น์ ์ฌ์ด์ ๋ผ์ด๋ค์ด ์ด๋ฒคํธ ์คํธ๋ฆผ์ ๊ฐ๊ณตํ๋ ๊ณ์ธต
agent.runAgent()ํธ์ถ ์ ๋ฑ๋ก๋ ๋ฏธ๋ค์จ์ด ์ฒด์ธ์ ํต๊ณผํ๋ RxJS Observable ๊ธฐ๋ฐ ํ์ดํ๋ผ์ธ- ํต์ฌ ์ฑ ์์ ์ด๋ฒคํธ ๋ณํยทํํฐ๋งยท๋ฉํ๋ฐ์ดํฐ ์ฃผ์ ยท์๋ฌ ์ฒ๋ฆฌยท๋ชจ๋ํฐ๋ง์ 5๊ฐ์ง ํก๋จ ๊ด์ฌ์ฌ(cross-cutting concerns)
- agent ๋ณธ์ฒด ์ฝ๋๋ฅผ ์์ ํ์ง ์๊ณ ๊ธฐ๋ฅ์ ํ์ฅํ๋ ์ํ(onion) ๊ตฌ์กฐ์ ๋ฐ์ฝ๋ ์ดํฐ ํจํด
ํด๋น ๊ฐ๋ ์ด ํ์ํ ์ด์
- agent์ ํต์ฌ ๋ก์ง๊ณผ ๋ถ๊ฐ ๊ธฐ๋ฅ(๋ก๊น , ์ธ์ฆ, ํํฐ๋ง)์ ๋ถ๋ฆฌํด ๋จ์ผ ์ฑ ์ ์์น ์ ์ง
- ๋์ผํ ์์ด์ ํธ๋ฅผ ๋ค์ํ ํ๊ฒฝ(๊ฐ๋ฐ/์ด์, ๋๋ฒ๊ทธ/ํ๋ก๋์ )์์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅ
- ์ด๋ฒคํธ ์คํธ๋ฆผ์ ์ผ๋ฅ ์ ์ผ๋ก ์ ์ฉ๋๋ ์ ์ฑ (์: ์์ค ํํฐ)์ ์ ์ธ์ ์ผ๋ก ์กฐํฉ
- ์ธ์ฆยท๊ถํยทrate limit ๊ฐ์ ์ ์ฑ ์ agent ์ธ๋ถ์์ ๊ฐ์ ํด ๋ณด์ ๊ฒฝ๊ณ ๋ช ํํ
AS-IS
๋ฏธ๋ค์จ์ด ์์ด agent ์์ ๋ชจ๋ ๋ถ๊ฐ ๊ธฐ๋ฅ์ด ์์ธ ๊ตฌ์กฐ:
class MyAgent extends AbstractAgent {
run(input: RunAgentInput): Observable<BaseEvent> {
console.log("[REQ] start") // ๋ก๊น
if (!input.forwardedProps?.token) throw Error // ์ธ์ฆ
return this.callBackend(input).pipe(
filter(e => !this.isDangerous(e)), // ๋๊ตฌ ํํฐ
map(e => this.maskProfanity(e)), // ์์ค ๋ง์คํน
tap(e => this.recordMetric(e)), // ๋ฉํธ๋ฆญ
finalize(() => console.log("[DONE]")) // ์ข
๋ฃ ๋ก๊ทธ
)
}
}TO-BE
๋ฏธ๋ค์จ์ด๋ก ๊ด์ฌ์ฌ๋ฅผ ๋ถ๋ฆฌํ ๊ตฌ์กฐ:
flowchart LR UI[UI: runAgent input] --> A[authMiddleware] A --> P[profanityMiddleware] P --> L[loggingMiddleware] L --> F[filterToolCallsMiddleware] F --> AG[agent.run] AG -. event .-> F F -. event .-> L L -. event .-> P P -. event .-> A A -. event .-> UI
agent.use(authMiddleware, profanityMiddleware, loggingMiddleware, filterToolCallsMiddleware)
await agent.runAgent(input)Middleware์ ๋ ํ์ด์ฆ
Middleware๋ ์์ ์ด ๋ค๋ฅธ ๋ ํ๋ฆ์ ๋์์ ๋ค๋ฃฌ๋ค.
| ํ์ด์ฆ | ์์ | ํ์ | RxJS ๊ด์ |
|---|---|---|---|
| Phase 1: ์ ๋ ฅ | ์ฌ์ฉ์๊ฐ ๋ฉ์์ง ๋ณด๋ผ ๋ | 1ํ | subscribe() ์์ ๋๊ธฐ ํจ์ ํธ์ถ |
| Phase 2: ์ด๋ฒคํธ | AI ์๋ต์ด ํ๋ฌ๋์ฌ ๋ | Nํ | ๋น๋๊ธฐ emit per event |
ํ ๋ฏธ๋ค์จ์ด ์ฝ๋ ์์์ ๋ ํ์ด์ฆ ์์ญ์ด ๋ช ํํ ๊ตฌ๋ถ๋๋ค:
const myMiddleware: MiddlewareFunction = (input, next) => {
// โโ Phase 1 ์์ญ: ์
๋ ฅ ๊ฐ๊ณต (1ํ ์คํ) โโ
const modifiedInput = { ...input, /* ... */ }
return next.run(modifiedInput).pipe(
// โโ Phase 2 ์์ญ: ์ด๋ฒคํธ ๊ฐ๊ณต (event๋ง๋ค ์คํ) โโ
map(event => /* ... */),
tap(event => /* ... */),
finalize(() => /* ์ข
๋ฃ ์ 1ํ */)
)
}์ํ(Onion) ํจํด ๋์ ๋ชจ๋ธ
Phase 1 โ ์ ๋ ฅ์ด ์์ชฝ์ผ๋ก ํ๋ฆ
sequenceDiagram autonumber participant UI participant Auth as authMiddleware participant Prof as profanityMiddleware participant Log as loggingMiddleware participant Filter as filterToolCallsMiddleware participant Agent as agent.run UI->>Auth: runAgent(input) Auth->>Auth: forwardedProps์ ํ ํฐ ์ฃผ์ Auth->>Prof: next.run(authedInput) Prof->>Prof: messages ์์ค ๋ง์คํน Prof->>Log: next.run(cleanedInput) Log->>Log: "[REQ] runId=..." ๊ธฐ๋ก Log->>Filter: next.run(input) Filter->>Agent: next.run(input) Agent->>Agent: ๋ฐฑ์๋๋ก SSE ์์ฒญ
Phase 2 โ ์ด๋ฒคํธ๊ฐ ๋ฐ๊นฅ์ชฝ์ผ๋ก ํ๋ฆ
sequenceDiagram autonumber participant UI participant Auth as authMiddleware participant Prof as profanityMiddleware participant Log as loggingMiddleware participant Filter as filterToolCallsMiddleware participant Agent as agent.run loop ๋งค ์ด๋ฒคํธ๋ง๋ค Agent->>Filter: emit BaseEvent Filter->>Filter: ์ํ ๋๊ตฌ๋ฉด DROP Filter->>Log: pass event Log->>Log: tap "[EVT] type" Log->>Prof: pass event Prof->>Prof: TEXT_MESSAGE_CONTENT ๋ง์คํน Prof->>Auth: pass event Auth->>UI: ์ต์ข ์ด๋ฒคํธ ์ ๋ฌ end
ํต์ฌ ๋น๋์นญ์ ์
๋ ฅ์ 1๋ฒ, ์ด๋ฒคํธ๋ ๋งค emit๋ง๋ค ์ฒด์ธ์ ํต๊ณผํ๋ค๋ ์ ์ด๋ค. ๊ทธ๋์ pipe(...) ์์ map/tap์ ์ด๋ฒคํธ ๊ฐ์๋งํผ ์คํ๋๋ค.
์ด๋ฒคํธ๋ ์ฒญํฌ ๋จ์๋ก ํ๋ฅธ๋ค
Phase 2๊ฐ โ์ด๋ฒคํธ๋ง๋ค N๋ฒ ํธ์ถ๋๋คโ๊ณ ํ์ ๋, ๊ทธ N์ ์ ์ฒด๋ฅผ ์ ํํ ์ดํดํด์ผ ๋ฏธ๋ค์จ์ด ๋ก์ง์ ์ฌ๋ฐ๋ฅด๊ฒ ์์ฑํ ์ ์๋ค. AG-UI์ ํ ์คํธ ๋ฉ์์ง์ ๋๊ตฌ ํธ์ถ์ ๋ ๋ค ์ฒญํฌ ๋จ์๋ก ๋ถํ ๋์ด ํ๋ฌ๋์ค๊ธฐ ๋๋ฌธ์ด๋ค(AG-UI Event ์ฐธ๊ณ ).
ํ ์คํธ ๋ฉ์์ง์ ์ฒญํฌ ๊ตฌ์กฐ
AI๊ฐ โ์๋ ํ์ธ์ ๋ฐ๊ฐ์ต๋๋คโ๋ฅผ ์คํธ๋ฆฌ๋ฐํ ๋ ๋ฐฑ์๋๋ ๋ค์๊ณผ ๊ฐ์ด ๋ถ๋ฆฌ๋ ์ด๋ฒคํธ๋ค์ ์์ฐจ emitํ๋ค:
TEXT_MESSAGE_START (messageId)
TEXT_MESSAGE_CONTENT (delta="์๋
")
TEXT_MESSAGE_CONTENT (delta="ํ์ธ์")
TEXT_MESSAGE_CONTENT (delta=" ๋ฐ๊ฐ")
TEXT_MESSAGE_CONTENT (delta="์ต๋๋ค")
TEXT_MESSAGE_END (messageId)
์ด 6๊ฐ ์ด๋ฒคํธ ๊ฐ๊ฐ์ด ๋ฏธ๋ค์จ์ด ์ฒด์ธ์ ํต๊ณผํ๋ค. ๋ฐ๋ผ์ profanityMiddleware์ pipe(map(...))์ 6๋ฒ ํธ์ถ๋๋ฉฐ, delta๊ฐ ๋ 4๋ฒ์์๋ง ๋ง์คํน ๋ก์ง์ด ์ค์ ๋ก ๋์ํ๋ค.
AG-UI๋ ๋์ผํ ์ ๋ณด๋ฅผ ๋ ์ปดํฉํธํ๊ฒ ํํํ๋ TEXT_MESSAGE_CHUNK ํํ๋ ์ง์ํ๋ค. ํ ์ด๋ฒคํธ์ messageId+delta๊ฐ ๋ฌถ์ฌ ๋ค์ด์ค๊ณ ๋ณ๋ START/END๊ฐ ์๋ ํํ๋ค. ๋ฏธ๋ค์จ์ด๋ ์์ชฝ ํํ๋ฅผ ๋ชจ๋ ์ฒ๋ฆฌํ๋๋ก ์์ฑํ๋ ๊ฒ ์์ ํ๋ค.
๋๊ตฌ ํธ์ถ๋ ๋ง์ฐฌ๊ฐ์ง
TOOL_CALL_START (toolCallId, toolCallName="search")
TOOL_CALL_ARGS (delta='{"query":')
TOOL_CALL_ARGS (delta='"AG-UI"}')
TOOL_CALL_END (toolCallId)
๋๊ตฌ ์ธ์(JSON ๋ฌธ์์ด)๋ ์ฒญํฌ ๋จ์๋ก emit๋๋ฏ๋ก ํ ํธ์ถ๋น ARGS ์ด๋ฒคํธ๊ฐ ์ฌ๋ฌ ๋ฒ ๋ฐ์ํ๋ค. ๋ฏธ๋ค์จ์ด๋ โ์์ฑ๋ ์ธ์ ๊ฐ์ฒดโ๋ฅผ ํ ๋ฒ์ ๋ฐ๋ ๊ฒ ์๋๋ผ JSON ์กฐ๊ฐ์ ๋์ ํด์ผ ํ๋ ์ํฉ์ ํญ์ ์ ์ ํด์ผ ํ๋ค.
์์ฑ ๋ฐฉ์ ๋ ๊ฐ์ง
Function-Based โ ๋จ์ ๋ณํ์ฉ
const prefixMiddleware: MiddlewareFunction = (input, next) => {
return next.run(input).pipe(
map(event => {
if (event.type === EventType.TEXT_MESSAGE_CONTENT) {
return { ...event, delta: `[AI]: ${event.delta}` }
}
return event
})
)
}์๊ทธ๋์ฒ: (input: RunAgentInput, next: AbstractAgent) => Observable<BaseEvent>
Class-Based โ ์ํ/์ค์ ํ์ ์
class MetricsMiddleware extends Middleware {
private eventCount = 0
constructor(private metricsService: MetricsService) { super() }
run(input: RunAgentInput, next: AbstractAgent): Observable<BaseEvent> {
const start = Date.now()
return this.runNext(input, next).pipe(
tap(event => {
this.eventCount++
this.metricsService.recordEvent(event.type)
}),
finalize(() => {
this.metricsService.recordDuration(Date.now() - start)
})
)
}
}์ฒญํฌ๋ฅผ ์๋ฏธ ๋จ์๋ก ๋ฐ๊ธฐ โ runNext์ runNextWithState
์ฒญํฌ ๋ถํ ์ ์กด์ฌ๋ฅผ ์๋ฉด ์์ฐ์ค๋ฝ๊ฒ ์๋ฌธ์ด ์๊ธด๋ค. โ๋งค๋ฒ ์ฒญํฌ๋ฅผ ์ง์ ๋์ ํด์ผ ํ๋?โ ํด๋์ค ๋ฏธ๋ค์จ์ด๊ฐ ์ ๊ณตํ๋ ๋ ํฌํผ๊ฐ ์ด ๋ถ๋ถ์ ๋์์ค๋ค. ๋จ, ํฌํผ์ ์๋ฏธ๋ฅผ ์ ํํ ์์์ผ ์คํด๊ฐ ์๋ค.
runNext() โ ํฉ์น๊ธฐ๊ฐ ์๋๋ผ โ์ ๊ทํโ
runNext()์ ์ญํ ์ ์ฒญํฌ๋ค์ ํ๋์ ์ด๋ฒคํธ๋ก ํฉ์ณ์ฃผ๋ ๊ฒ์ด ์๋๋ผ, ์ด๋ค ํํ๋ก ๋ค์ด์ค๋ ์ผ๊ด๋ ๋ถ๋ฆฌํ(START/CONTENT/END)์ผ๋ก ๋ณํํด์ฃผ๋ ๊ฒ์ด๋ค.
[์
๋ ฅ โ CHUNK ํํ๋ก ๋ค์ด์๋ค๊ณ ๊ฐ์ ]
TEXT_MESSAGE_CHUNK(delta="์๋
")
TEXT_MESSAGE_CHUNK(delta="ํ์ธ์")
TEXT_MESSAGE_CHUNK(delta=" ๋ฐ๊ฐ")
TEXT_MESSAGE_CHUNK(delta="์ต๋๋ค")
[runNext() ํต๊ณผ ํ โ ๋ถ๋ฆฌํ์ผ๋ก ์ ๊ทํ]
TEXT_MESSAGE_START
TEXT_MESSAGE_CONTENT(delta="์๋
")
TEXT_MESSAGE_CONTENT(delta="ํ์ธ์")
TEXT_MESSAGE_CONTENT(delta=" ๋ฐ๊ฐ")
TEXT_MESSAGE_CONTENT(delta="์ต๋๋ค")
TEXT_MESSAGE_END
์ด๋ฒคํธ ๊ฐ์๋ ๊ทธ๋๋ก(๋๋ START/END๊ฐ ์ถ๊ฐ๋์ด ๋ ๋ง์์ง)์ด๋ฉฐ, ๋ฏธ๋ค์จ์ด pipe๋ ์ฌ์ ํ N๋ฒ ํธ์ถ๋๋ค. runNext๊ฐ ๋ณด์ฅํ๋ ๊ฑด **โ๋ ๊ฐ์ง ์
๋ ฅ ํํ๋ฅผ ํ ๊ฐ์ง๋ก ํต์ผํด ๋ถ๊ธฐ ์ฒ๋ฆฌ๋ฅผ ์ค์ฌ์ฃผ๋ ๊ฒโ**์ด์ง ์ฒญํฌ ํตํฉ์ด ์๋๋ค.
์์ฑ๋ ๋ฉ์์ง ํ ๋ฉ์ด๋ฆฌ๋ฅผ ๋ฐ๋ ๋ ๊ฐ์ง ํจํด
์ฒญํฌ๊ฐ ์๋๋ผ ์์ฑ๋ ํ ์คํธ ํ ๋ฉ์ด๋ฆฌ๋ฅผ ๋ค๋ฃจ๊ณ ์ถ๋ค๋ฉด ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ด ์๋ค.
๋ฐฉ๋ฒ A โ TEXT_MESSAGE_END ์์ ์ ์ง์ ๋์
class FullMessageMiddleware extends Middleware {
private buffer = ""
run(input, next) {
return this.runNext(input, next).pipe(
tap(event => {
if (event.type === EventType.TEXT_MESSAGE_CONTENT) {
this.buffer += event.delta
}
if (event.type === EventType.TEXT_MESSAGE_END) {
console.log("์์ฑ๋ ๋ฉ์์ง:", this.buffer)
this.buffer = ""
}
})
)
}
}๋ฐฉ๋ฒ B โ runNextWithState()์ ๋์ messages ํ์ฉ
class StateMiddleware extends Middleware {
run(input, next) {
return this.runNextWithState(input, next).pipe(
tap(({ event, messages, state }) => {
const lastMsg = messages[messages.length - 1]
if (event.type === EventType.TEXT_MESSAGE_END) {
console.log("์์ฑ๋ ๋ฉ์์ง:", lastMsg.content)
}
})
)
}
}runNextWithState()๋ ์ด๋ฒคํธ๋ ๊ทธ๋๋ก N๋ฒ emitํ๋, ๊ฐ ์์ ์ **โ์ง๊ธ๊น์ง ๋์ ๋ messages์ stateโ**๋ฅผ ํจ๊ป ์ ๊ณตํ๋ค. ์ฒญํฌ ๋์ ๋ก์ง์ ์ง์ ์งค ํ์ ์์ด ๋ง์ง๋ง ๋ฉ์์ง๋ฅผ ์ฝ๊ธฐ๋ง ํ๋ฉด ๋๋ค.
์ธ ๋จ๊ณ ๋น๊ต
| ํฌํผ | ์ด๋ฒคํธ ๊ฐ์ ๋ณํ | ์ ๊ณตํ๋ ๊ฒ |
|---|---|---|
next.run() (raw) | ๊ทธ๋๋ก | event๋ง |
runNext() | CHUNK โ START/CONTENT/END๋ก ํํ ํต์ผ | event๋ง, ํํ ์ผ๊ด์ฑ |
runNextWithState() | ์์ ๋์ผ | event + ๋์ messages + ๋์ state |
์์ฝํ์๋ฉด โ์ด๋ฒคํธ๋ฅผ 1๊ฐ๋ก ํฉ์ณ์ฃผ๋ ํฌํผ๋ ์๋คโ. ํฉ์น๊ณ ์ถ๋ค๋ฉด END ์์ ์ buffer๋ฅผ ๋น์ฐ๊ฑฐ๋ runNextWithState์ ๋์ messages๋ฅผ ์ฝ์ด์ผ ํ๋ค.
๋ด์ฅ Middleware โ FilterToolCallsMiddleware
๋๊ตฌ ํธ์ถ ์ด๋ฒคํธ๋ฅผ ํ์ดํธ๋ฆฌ์คํธ/๋ธ๋๋ฆฌ์คํธ๋ก ํํฐ๋งํ๋ค.
import { FilterToolCallsMiddleware } from "@ag-ui/client"
const allow = new FilterToolCallsMiddleware({
allowedToolCalls: ["search", "calculate"]
})
const block = new FilterToolCallsMiddleware({
disallowedToolCalls: ["delete", "modify"]
})
agent.use(allow)์ด ๋ฏธ๋ค์จ์ด๊ฐ ์ฐจ๋จํ๋ ๊ฑด ์ด๋ฏธ ๋ฐฉ์ถ๋ TOOL_CALL_* ์ด๋ฒคํธ๋ค. ๋ชจ๋ธ์ด๋ ๋ฐฑ์๋ ๋ฐํ์์์ ๋๊ตฌ๊ฐ ์ค์ ๋ก ์คํ๋๋ ๊ฒ์ ๋ง๋ ๊ฒ ์๋๋ค. ์ด ์ฐจ์ด๋ ๋๊ตฌ ์คํ ์ฃผ์ฒด๊ฐ ์ด๋๋์ ๋ฐ๋ผ ๊ฒฐ๊ณผ๊ฐ ํฌ๊ฒ ๋ฌ๋ผ์ง๋ฏ๋ก ๋ณ๋๋ก ์ง์ ํ์๊ฐ ์๋ค.
Backend vs Frontend ๋๊ตฌ โ ์ฐจ๋จ์ ๋น๋์นญ
AG-UI์์ ๋๊ตฌ๋ ๋ ๊ตฐ๋ฐ์์ ์คํ๋ ์ ์๊ณ , ์ฐจ๋จ๋๋ ์์ ์ ๋์ผํ๊ฒ Phase 2(์ด๋ฒคํธ ๋ฐํ ํ๋ฆ)์ง๋ง ์คํ ์์ ์ด ๋ฌ๋ผ ๊ฒฐ๊ณผ๊ฐ ๋ค๋ฅด๋ค.
Backend ๋๊ตฌ โ ์คํ์ ๋๋ฌ๊ณ ํ์๋ง ๊ฐ๋ ค์ง๋ค
sequenceDiagram autonumber participant UI participant MW as filterToolCallsMiddleware participant Agent as ๋ฐฑ์๋ agent participant Tool as bash ์คํ๊ธฐ UI->>MW: Phase 1: runAgent(input) MW->>Agent: input ํต๊ณผ Agent->>Tool: bash ์ค์ ์คํ (์๋ฒ์์) Tool-->>Agent: ๊ฒฐ๊ณผ ๋ฐํ Note over Agent,Tool: ์คํ์ ์ด๋ฏธ ๋๋จ Agent->>MW: Phase 2: TOOL_CALL_START(name='bash') MW->>MW: ์ฐจ๋จ ๋์ โ DROP Note over MW,UI: UI๋ ์ด ์ด๋ฒคํธ๋ฅผ ๋ชป ๋ด Agent->>MW: TEXT_MESSAGE_CONTENT(๊ฒฐ๊ณผ ์์ฝ) MW->>UI: ํต๊ณผ
๋๊ตฌ ์คํ์ด ์ด๋ฒคํธ๊ฐ emit๋๊ธฐ ์ ์ ์ด๋ฏธ ๋ฐฑ์๋์์ ๋๋ฌ๋ค. ๋ฏธ๋ค์จ์ด๊ฐ ๋ณด๋ TOOL_CALL_*๋ โ์คํ๋๋ค๋ ์๋ฆผโ์ผ ๋ฟ์ด๋ฏ๋ก ์ฐจ๋จํด๋ ์คํ์๋ ์ํฅ์ด ์๊ณ UI ํ์๋ง ๊ฐ๋ ค์ง๋ค. ๋ด๋ถ ํฌํผ ๋๊ตฌ๋ ๋๋ฒ๊ทธ์ฉ ๊ฒ์์ฒ๋ผ UI ๋
ธ์ด์ฆ๋ฅผ ์ค์ด๊ณ ์ถ์ ๋ ์ ์ฉํ๋ค.
Frontend ๋๊ตฌ โ ํธ๋ฆฌ๊ฑฐ ์์ฒด๊ฐ ์ฌ๋ผ์ง๋ค
Frontend ๋๊ตฌ๋ ๋ฐฑ์๋๊ฐ ์คํํ์ง ์๊ณ , โUI์ผ, ์ด ๋๊ตฌ ์ข ์คํํด์คโ๋ผ๋ ์์ฒญ ์ด๋ฒคํธ๋ง ๋ณด๋ธ๋ค. UI๊ฐ ๊ทธ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ ์ง์ ์คํํ๋ค(์: confirm() ๋ค์ด์ผ๋ก๊ทธ, DOM ์กฐ์, ๋ก์ปฌ ํ์ผ ์ ๊ทผ).
sequenceDiagram autonumber participant UI participant MW as filterToolCallsMiddleware participant Agent as ๋ฐฑ์๋ agent participant FETool as UI ๋ด๋ถ์<br/>tool executor UI->>MW: Phase 1: runAgent(input) MW->>Agent: input ํต๊ณผ Agent->>Agent: LLM์ด 'confirm()' ๋๊ตฌ ํธ์ถ ๊ฒฐ์ Note over Agent: ๋ฐฑ์๋๋ ์คํ ์ ํจ<br/>"UI์ ์์" ๋ชจ๋ Agent->>MW: Phase 2: TOOL_CALL_START(name='confirm') MW->>MW: ์ฐจ๋จ ๋์ โ DROP Note over MW,UI: UI๋ ์ด๋ฒคํธ๋ฅผ ๋ชป ๋ด Note over UI,FETool: ๋๊ตฌ ์คํ ํธ๋ฆฌ๊ฑฐ ์์ฒด๊ฐ<br/>์ค์ง ์์ โ ์คํ ์ ๋จ
๋๊ตฌ ์คํ์ด ์ด๋ฒคํธ๊ฐ UI์ ๋์ฐฉํด์ผ ๋น๋ก์ ํธ๋ฆฌ๊ฑฐ๋๋ค. ๋ฏธ๋ค์จ์ด๊ฐ ๊ทธ ์ด๋ฒคํธ๋ฅผ ์ฐจ๋จํ๋ฉด UI๋ ํธ๋ฆฌ๊ฑฐ๋ฅผ ๋ฐ์ง ๋ชปํ๊ณ , ์คํ ์์ฒด๊ฐ ์ผ์ด๋์ง ์๋๋ค.
๊ฐ์ ์ฐจ๋จ, ๋ค๋ฅธ ๊ฒฐ๊ณผ
| ์ผ์ด์ค | ๋๊ตฌ ์คํ ์์ | ๋ฏธ๋ค์จ์ด๊ฐ ์ฐจ๋จํ๋ ๊ฒ | ์ฐจ๋จ์ ํจ๊ณผ |
|---|---|---|---|
| Backend ๋๊ตฌ | Phase 2 emit ์ | TOOL_CALL_* ์ด๋ฒคํธ | UI ํ์๋ง ์ฐจ๋จ. ์คํ์ ์ด๋ฏธ ๋๋จ |
| Frontend ๋๊ตฌ | Phase 2 emit์ ํธ๋ฆฌ๊ฑฐ๋ก ์ฌ์ฉ | TOOL_CALL_* ์ด๋ฒคํธ | ํธ๋ฆฌ๊ฑฐ ์์ฒด๊ฐ ์ฐจ๋จ โ ์คํ ์ ๋จ |
์ด ๋น๋์นญ ๋๋ฌธ์ FilterToolCallsMiddleware๋ฅผ ๋ณด์ ํต์ ๋ก ์ฐ๋ฉด ์ ๋๋ค. ์ง์ง ์ํํ ๋๊ตฌ๋ฅผ ์ฐจ๋จํ๋ ค๋ฉด:
- Backend ๋๊ตฌ๋ ๋ฐฑ์๋ ๊ถํ ์์คํ ์์ ์ฐจ๋จํด์ผ ํ๋ค. ๋ฏธ๋ค์จ์ด๋ ๋ชจ๋ธ ํธ์ถ๊ณผ ์ค์ ์คํ์ ๋ง์ ์ ์๋ค.
- Frontend ๋๊ตฌ๋ ๋ฏธ๋ค์จ์ด๋ก ์ด๋ฒคํธ๋ฅผ ์ฐจ๋จํ๋ฉด ์คํ์ ๋ง์ ์ ์์ผ๋, ๋ชจ๋ธ์ ์ฌ์ ํ ํธ์ถ ์๋๋ฅผ ๊ฐ์ง๊ณ ์๋ค. ๋ชจ๋ธ ์ธก ๊ฐ๋๋ ํจ๊ป ํ์ํ๋ค.
๋ฏธ๋ค์จ์ด ํํฐ์ ์ง์ง ์ฉ๋๋ ๋ณด์์ด ์๋๋ผ UI ๋ ธ์ด์ฆ ๊ฐ์๋ผ๊ณ ์ ๋ฆฌํ ์ ์๋ค.
์ค์ ํจํด 5๊ฐ์ง
โ Auth โ Phase 1๋ง ์ฌ์ฉ
const authMiddleware: MiddlewareFunction = (input, next) => {
const token = localStorage.getItem("jwt")
return next.run({
...input,
forwardedProps: { ...input.forwardedProps, authorization: `Bearer ${token}` }
})
}โก Tool Filter โ Phase 2 ์ฐจ๋จ
const dangerousTools = new Set(["deleteAccount", "transferFunds"])
const filterDangerousMiddleware: MiddlewareFunction = (input, next) => {
let blockedId: string | null = null
return next.run(input).pipe(
filter(event => {
if (event.type === EventType.TOOL_CALL_START && dangerousTools.has(event.toolCallName)) {
blockedId = event.toolCallId
return false
}
if ((event.type === EventType.TOOL_CALL_ARGS || event.type === EventType.TOOL_CALL_END)
&& event.toolCallId === blockedId) {
return false
}
return true
})
)
}โข Profanity Filter โ Phase 2 ๋ณํ
const profanityMiddleware: MiddlewareFunction = (input, next) => {
return next.run(input).pipe(
map(event => {
if (event.type === EventType.TEXT_MESSAGE_CONTENT ||
event.type === EventType.TEXT_MESSAGE_CHUNK) {
return { ...event, delta: maskProfanity(event.delta) }
}
return event
})
)
}โฃ Logging โ ์์ชฝ ํ์ด์ฆ
const loggingMiddleware: MiddlewareFunction = (input, next) => {
console.log(`[REQ] runId=${input.runId} threadId=${input.threadId}`)
const start = Date.now()
return next.run(input).pipe(
tap(event => console.log(`[EVT] ${event.type}`)),
finalize(() => console.log(`[DONE] ${Date.now() - start}ms`))
)
}โค Rate Limit โ Phase 1 ์ง์ฐ
let lastRunAt = 0
const MIN_INTERVAL = 2000
const rateLimitMiddleware: MiddlewareFunction = (input, next) => {
const wait = Math.max(0, lastRunAt + MIN_INTERVAL - Date.now())
lastRunAt = Date.now() + wait
return timer(wait).pipe(switchMap(() => next.run(input)))
}๋ฑ๋ก ์์์ ๋ฐฐ์น ์ ๋ต
agent.use(authMiddleware, profanityMiddleware, loggingMiddleware, filterMiddleware)| ํ์ด์ฆ | ํ๋ฆ ๋ฐฉํฅ | ์์ |
|---|---|---|
| Phase 1 (์ ๋ ฅ) | ๋ฐ๊นฅ โ ์์ชฝ | auth โ profanity โ logging โ filter โ agent |
| Phase 2 (์ด๋ฒคํธ) | ์์ชฝ โ ๋ฐ๊นฅ | agent โ filter โ logging โ profanity โ auth โ UI |
๋ฐฐ์น ๊ฐ์ด๋:
- ์ธ์ฆ/์ธ๊ฐ โ ๊ฐ์ฅ ๋ฐ๊นฅ (์์ฒญ ์ฐจ๋จ ์ ์์ชฝ ์คํ ์ ๋จ)
- ๋ก๊น /๋ฉํธ๋ฆญ โ ์ค๊ฐ (๋ณํ๋ ์ ๋ ฅ๊ณผ ๋ณํ๋ ์ด๋ฒคํธ ๋ชจ๋ ๊ด์ฐฐ)
- ์ด๋ฒคํธ ํํฐ/๋ณํ โ ๊ฐ์ฅ ์์ชฝ (agent์ ๊ฐ๊น์ด raw ์ด๋ฒคํธ ์ง์ ์ฒ๋ฆฌ)
์ฃผ์์ฌํญ ๋ฐ ํจ์
connectAgent()๋ ๋ฏธ๋ค์จ์ด๋ฅผ ๊ฑฐ์น์ง ์๋๋ค โconnect()๋ฅผ ์ง์ ํธ์ถํ๋ฏ๋ก ์ธ์ฆยทํํฐ ๋ฑ์ด ์ฐํ๋๋ค. ๋ฏธ๋ค์จ์ด๋runAgent()์์๋ง ์ ์ฉ๋๋ค.FilterToolCallsMiddleware๋ ๋ณด์ ํต์ ๊ฐ ์๋๋ค โ ์ด๋ฒคํธ๋ง ๊ฐ๋ฆด ๋ฟ ์ค์ ๋๊ตฌ ์คํ์ ๋ง์ง ์๋๋ค. Frontend ๋๊ตฌ์ ๊ฒฝ์ฐ ํธ๋ฆฌ๊ฑฐ๊ฐ ์ฐจ๋จ๋์ด ์คํ์ ๋งํ์ง๋ง, ๋ชจ๋ธ์ ์ฌ์ ํ ํธ์ถ ์๋๋ฅผ ๊ฐ์ง๋ค.pipe์ ์ฐ์ฐ์ ์ด๋ฒคํธ ๊ฐ์๋งํผ ์คํ๋๋ค โ ํ ์คํธ์ ๋๊ตฌ ์ธ์๊ฐ ์ฒญํฌ๋ก ๋ถํ ๋๋ฏ๋ก ๋ฌด๊ฑฐ์ด ๋๊ธฐ ์ฐ์ฐ์ ๋ฃ์ผ๋ฉด ์คํธ๋ฆผ ์ ์ฒด๊ฐ ๋๋ ค์ง๋ค.- ์๋ฌ๋ RxJS ์คํผ๋ ์ดํฐ๋ก ์ฒ๋ฆฌ โ
try/catch๊ฐ ์๋catchError๋ก ๊ฐ์ธ์ผ ์คํธ๋ฆผ์ด ํต์งธ๋ก ์ฃฝ์ง ์๋๋ค. - ์ํ๋ฅผ ๊ฐ์ง๋ ๋ฏธ๋ค์จ์ด๋ ์ธ์คํด์ค ๊ณต์ ์ฃผ์ โ ๋์ผ ์ธ์คํด์ค๋ฅผ ์ฌ๋ฌ agent์ ๋ฑ๋กํ๋ฉด ์นด์ดํฐยท๋ฒํผ ๋ฑ์ด ์์ธ๋ค.
Best Practices
- Single responsibility โ ํ ๋ฏธ๋ค์จ์ด = ํ ๊ฐ์ง ์ผ
- Graceful error handling โ
catchError๋ก ์คํธ๋ฆผ ๋ณดํธ - Non-blocking โ I/O๋
switchMap+from(promise)๋ฑ async ํจํด - Side-effect ๋ช ์ โ state ์์ ์ ๋ฌธ์ํ
- ๋
๋ฆฝ ํ
์คํธ โ mock
nextagent๋ก ๋จ์ ํ ์คํธ - ์ฑ๋ฅ ์ธ์ง โ ๋ชจ๋ ์ด๋ฒคํธ๊ฐ ํต๊ณผํ๋ฏ๋ก ๋ฌด๊ฑฐ์ด ์ฐ์ฐ ๊ธ์ง