향상된 챗봇 에이전트의 Workflow를 소개합니다
들어가며
이 글은 Omnilude의 ai-service는 왜 Assistant / Thread / Run 구조를 택했을까, Omnilude의 ai-service에서 Agent는 이렇게 동작합니다의 다음 이야기입니다.
앞선 글에서는 구조를 설명했습니다. 이번에는 그 구조가 실제로 어떤 모습으로 굴러가고 있는지 보겠습니다. 설명 대상은 향상된 챗봇입니다.
이 글은 추상적인 예시가 아니라 실제 작성된 workflow 구성을 바탕으로, 이 에이전트가 어떤 입력을 받고 어디서 분기하며 어떤 assistant와 tool을 거쳐 답을 만드는지 그대로 설명해보겠습니다.
향상된 챗봇은 한 문장으로 무엇인가
이 에이전트는 질문을 네 갈래로 나눠서 처리하는 비교적 노골적인 분기형 챗봇입니다.
- 일반 질문이면 바로 답합니다.
- 최신 정보가 필요하면 검색 경로로 보냅니다.
- YouTube URL이면 자막 요약 경로로 보냅니다.
- 일반 웹 URL이면 아티클 분석 경로로 보냅니다.
실제 워크플로우를 단순화하면 아래와 같습니다.
즉, 이 워크플로우의 핵심은 모델 하나를 무작정 잘 부르는 데 있지 않습니다. 먼저 질문의 종류를 정하고, 그 뒤에 맞는 처리 파이프라인을 고르는 데 있습니다.
직접 사용해볼 수 있게 공개하고 싶지만, LLM 사용에 비용이 들어가서 아직 공개 배포는 하지 못하고 있습니다. 여유가 생기면 직접 실행해볼 수 있는 형태로도 열어보겠습니다.
시작은 텍스트 입력 하나입니다
이 에이전트의 시작 노드는 text-input이고, 예시 값으로는 최근 사회 이슈가 뭐야?가 들어 있습니다. 이 값은 단순한 데모 문구이기도 하지만, 동시에 이 워크플로우가 무엇을 해결하려고 하는지 잘 보여줍니다.
질문 하나를 받으면 바로 LLM에 던지는 대신, 먼저 router 노드로 보냅니다. 여기서 사용하는 assistant는 Question Router이고, 실제 설정된 라우팅 소스는 네 개입니다.
llm_direct: 직접 답변web_search: 웹 검색 후 답변youtube_summary: 유튜브 요약article_analyze: 웹 페이지 분석
이 라우터는 단순한 if 문이 아닙니다. assistant의 instructions 안에 어떤 상황에서 어떤 source를 골라야 하는지가 명시돼 있고, 응답도 반드시 JSON으로 반환하게 되어 있습니다. 예를 들어 최신 정보, 날짜 기준 정보, 팩트 체크가 필요한 질문은 web_search로 보내고, YouTube URL은 youtube_summary, 일반 URL은 article_analyze, 나머지는 llm_direct로 보냅니다.
이때 라우터는 source만 고르는 것이 아니라 reason도 함께 남깁니다. 캔버스에 붙어 있는 text-visualize 노드 하나는 바로 이 이유를 눈으로 확인하기 위한 디버깅 뷰어입니다. 즉, 이 화면은 단순한 설계도가 아니라 실행 흔적을 바로 확인하는 작업 화면입니다.
직답 경로는 가장 짧습니다
llm_direct로 분기되면 구조는 단순합니다. 라우터 출력이 Question Answer assistant를 쓰는 generate-text 노드로 들어가고, 이 노드의 출력은 그대로 finish로 향합니다.
여기서 중요한 건 이 경로가 단순하다고 해서 덜 구조적이지는 않다는 점입니다. generate-text 노드는 실행 시점에 aiAssistantId로 assistant를 다시 로드하고, system prompt와 user prompt를 조합한 뒤 실제 모델 호출을 수행합니다. 즉, 캔버스 안에 assistant 메타데이터가 붙어 있어도 런타임 기준은 결국 assistant id입니다.
향상된 챗봇에서 직답 경로에 연결된 assistant는 다음과 같습니다.
Router: Question Router (assistantId=1)
Direct answer: Question Answer (assistantId=2)
Search query writer: Web Search Expert (assistantId=12)
Search-based answer: AdaptiveAnswerer (assistantId=3)
YouTube writer: YoutubeSummarizer (assistantId=15)
Article writer: ArticleAnalizer (assistantId=16)
이 구성 덕분에 같은 generate-text 노드라도 어떤 assistant를 물리느냐에 따라 역할이 완전히 달라집니다. 노드는 범용 실행기이고, 역할은 assistant가 가져갑니다.
검색 경로는 두 단계가 아니라 네 단계입니다
이 에이전트의 진짜 핵심은 web_search 경로입니다. 현재 구현을 보면 검색 기반 답변을 한 번에 만들지 않습니다. 단계를 명확히 쪼개놨습니다.
첫 번째 단계는 Web Search Expert입니다. 이 assistant는 사용자의 질문을 바로 검색엔진에 던질 문장으로 바꿉니다. 예를 들어 모호한 질문을 그대로 검색하지 않고, 더 검색 친화적인 쿼리 한 줄로 정리하는 역할입니다.
두 번째 단계는 web-search-tool입니다. 현재 워크플로우 설정에는 searchTool: "searx"가 들어 있고, 실제 노드 구현은 SearXng를 호출해 검색 결과 스니펫을 최대 10건까지 모읍니다. 타임아웃도 200초로 잡아두어 무한 대기를 막고 있습니다.
세 번째 단계는 prompt-crafter입니다. 이 노드는 검색 결과를 바로 최종 답변에 붙이지 않습니다. 미리 저장된 템플릿에 {{results}}를 주입해서, 답변용 system prompt를 완성합니다. 템플릿 안에는 현재 시간, 참고 자료, 답변 지침, 글쓰기 톤이 모두 들어 있습니다.
네 번째 단계는 AdaptiveAnswerer입니다. 여기서 재미있는 점은 원래 사용자의 질문은 prompt로 유지되고, 검색으로 만든 컨텍스트는 system으로 들어간다는 점입니다. 즉, 검색 결과를 그냥 이어 붙이는 게 아니라, 질문과 근거를 분리한 상태에서 답변용 writer assistant가 다시 글을 씁니다.
이 흐름을 다시 그리면 이렇습니다.
저는 이 부분이 꽤 중요하다고 생각합니다. 검색을 붙인다고 해서 곧바로 RAG처럼 보이는 구조를 만들지 않고, 질문 정제와 근거 조립과 최종 작문을 따로 분리해두었기 때문입니다. 운영하면서 어느 단계가 흔들리는지 보기에도 훨씬 좋습니다.
YouTube와 Article 경로는 tool-first입니다
URL 계열 분기는 더 직관적입니다.
YouTube URL이면 youtube-summary-tool이 먼저 실행됩니다. 이 노드는 YoutubeTranscriptTool을 통해 시간 정보가 붙은 transcript를 가져옵니다. 그 다음 YoutubeSummarizer assistant가 이 transcript를 읽기 쉬운 콘텐츠로 다시 정리합니다.
일반 웹 URL이면 article-analyzer-tool이 먼저 실행됩니다. 이 노드는 Crawl4Ai로 페이지를 긁고, 가능하면 Readability4J로 본문만 정제해서 제목과 텍스트를 마크다운 형태로 만듭니다. 이후 ArticleAnalizer assistant가 이 내용을 읽기 쉬운 글로 재구성합니다.
즉, URL 계열 경로는 둘 다 같은 철학을 따릅니다.
- 먼저 도구가 원재료를 가져옵니다.
- 그 다음 assistant가 사람이 읽을 결과물로 정리합니다.
이 구조는 검색 경로와도 닮아 있습니다. Omnilude의 현재 agent 설계는 tool과 assistant를 경쟁 관계로 보지 않습니다. tool은 자료를 가져오고, assistant는 그 자료를 해석하고 정리합니다.
이 화면이 그냥 예쁜 다이어그램은 아닙니다
백오피스 캔버스를 보면 text-visualize 노드가 중간중간 붙어 있습니다. 이 노드들은 최종 결과를 만드는 데 필수는 아닙니다. 대신 지금 어떤 값이 흘러가고 있는지를 바로 보게 해줍니다.
실제 프론트 구현도 이 방향으로 맞춰져 있습니다. 워크플로우 실행 중 특정 노드가 NODE_COMPLETED 이벤트를 받으면, 프론트는 해당 노드 출력값을 캔버스 상태에 주입합니다. text-visualize 노드는 그 값을 그대로 표시하고, 스트리밍 델타가 오면 텍스트를 누적합니다.
이건 생각보다 중요한 차이입니다. 많은 워크플로우 툴이 노드를 그리는 데서 끝납니다. 하지만 현재 Omnilude 백오피스의 agent 화면은 저장된 그래프를 보는 용도와, 실제 실행 중간값을 관찰하는 용도가 합쳐져 있습니다. 그래서 이 캔버스는 설계도이면서 동시에 디버거입니다.
실행 엔진에서는 이렇게 받아들입니다
이제 캔버스 밖으로 내려가 보겠습니다. 이 워크플로우는 DB에서 ai.ai_agent.workflow JSONB로 저장됩니다. 별도 노드 테이블이 아니라 하나의 그래프 JSON으로 들고 있는 셈입니다.
실행이 시작되면 BasicWorkflowEngine이 먼저 type이 -input으로 끝나고 startNode=true인 노드를 찾습니다. 여기서는 text-input이 그 시작점입니다. 그 다음부터는 NodeExecutorFactory가 노드 타입에 맞는 executor를 골라서 실행을 이어갑니다.
향상된 챗봇에서 실제로 중요한 executor는 다음 정도로 요약할 수 있습니다.
TextInputNode: 시작 입력 준비RouterNode: assistant를 이용한 분기 선택GenerateTextNode: assistant 기반 텍스트 생성WebSearchToolNode: SearXng 검색YoutubeSummaryToolNode: 유튜브 자막 추출ArticleAnalyzerToolNode: 웹 본문 정제PromptCrafterNode: 템플릿 기반 system prompt 생성FinishNode: 각 분기 결과를 모아 종료
여기서 특히 중요한 것은 generate-text와 router 둘 다 실행 시점에 assistant를 다시 로드한다는 점입니다. 즉, agent가 직접 지능을 가지는 것이 아니라, 각 노드가 필요할 때 assistant를 불러 쓰는 구조입니다. 결국 agent는 orchestration이고, 실제 추론은 assistant가 담당합니다.
finish 노드도 생각보다 단순하지 않습니다
마지막 finish 노드는 그냥 “끝” 버튼이 아닙니다. 들어오는 edge들을 보고, 아직 진행 중인 입력 핸들이 있으면 기다립니다. 그리고 준비된 소스 노드들의 출력만 모아서 최종 결과로 넘깁니다.
이런 구조가 필요한 이유는 분기 때문입니다. 라우터에서 네 갈래로 나뉜다고 해서 네 갈래가 항상 동시에 완성되는 것은 아닙니다. 어떤 질문은 직답으로 끝나고, 어떤 질문은 검색과 요약 단계를 더 거칩니다. finish는 이런 차이를 흡수하는 마지막 수집기 역할을 합니다.
즉, 이 에이전트는 “질문 하나를 받고 답 하나를 내놓는 함수”가 아니라, 분기와 대기와 합류가 있는 그래프 실행기 위에 올라가 있습니다.
에이전트의 의도
간단한 이 에이전트가 Omnilude의 AI 활용 방향을 꽤 잘 보여준다고 생각합니다.
첫째, 범용 agent를 과장하지 않습니다. 질문 유형을 먼저 나누고, 그에 맞는 tool과 writer를 붙이는 식으로 현실적으로 설계돼 있습니다.
둘째, 검색 경로를 한 번에 뭉개지 않았습니다. 질문 정제, 검색, 컨텍스트 조립, 최종 답변을 각각 따로 떼어놨습니다.
셋째, 백오피스 캔버스가 단순 편집기가 아닙니다. 실제 실행 중간값을 볼 수 있게 만들어서, 프롬프트 실험과 디버깅을 같은 화면에서 하게 했습니다.
넷째, assistant와 agent의 역할이 분명합니다. assistant는 추론 부품이고, agent는 그 부품을 연결하는 워크플로우입니다.
이런 이유로 저는 이번 구현이 “AI agent를 만들었다”는 말보다 “질문 처리 파이프라인을 시각적으로 운영 가능한 형태로 만들었다”는 말에 더 가깝다고 봅니다.
마무리
앞선 글들에서 assistant, thread, run, agent의 구조를 설명했다면, 이번 글은 그 설명이 실제로 어떤 캔버스와 어떤 노드 연결로 나타나는지 보여주는 사례입니다. 향상된 챗봇은 라우터 하나, writer assistant 몇 개, 그리고 검색·유튜브·아티클 도구를 조합해 꽤 실용적인 챗봇으로 작동하고 있습니다.
여유가 되면 에이전트의 구성과 구현을 더 이해하기 쉽게, 이미지와 실제 돌아가는 화면을 바탕으로 다시 설명드릴 수 있으면 좋겠습니다.