
대화하며 발견한 내용을 노트에 쌓아간다 — Pendo 분석 어시스턴트에 온톨로지 계층을 추가했다 (dendo v2)
요약
Pendo의 데이터를 자연어로 분석할 수 있는 에이전트 기반 노트북 'Dendo v2'를 소개합니다. 자체 제작한 aggDSL을 사용하여 에이전트가 직접 쿼리를 생성하며, 대화 과정에서 얻은 유의미한 결과만을 노트에 셀 형태로 저장하고 관리할 수 있습니다.
핵심 포인트
- aggDSL을 활용한 에이전트의 자율적 쿼리 생성 및 실행
- 대화형 탐색을 통해 선별된 결과만 노트에 저장하는 워크플로우
- Jupyter Notebook 스타일의 살아있는(Live) 셀 구조 제공
- 온톨로지 계층 추가를 통한 데이터 분석 능력 강화
이전에 Pendo의 데이터를 자연어로 물어볼 수 있는 노트북 Dendo를 만들었다는 이야기를 썼습니다. 그때보다 훨씬 강력해져서 돌아왔습니다!
Dendo란 무엇인가?를 한마디로 설명하자면, Dendo는 Pendo 안의 프로덕트 이용 데이터에 agentic하게 질문할 수 있는 노트북입니다. "지난주 대시보드, 페르소나별 리텐션(Retention)은 어때?"라고 평문으로 물으면, 에이전트가 쿼리(Query)를 구성하여 Pendo에 던지고, 표와 그래프를 반환합니다. 좋은 결과물만 노트에 남겨둘 수 있습니다.
뒷단에서 움직이는 것은 Pendo MCP가 아닙니다. aggDSL이라는 자체 제작한 Pendo Aggregation API의 DSL(Domain Specific Language)로, 에이전트 스스로가 쿼리를 작성합니다.
v2의 핵심은 **온톨로지 계층 (Ontology Layer)**입니다만, 우선 앱 자체에 대한 이야기부터 하겠습니다. Dendo의 장점은 사실 훨씬 소박한 곳에 있습니다.
스크린샷은 헤드리스 브라우저(Headless Browser)로 촬영한 관계로 모두 영어로 되어 있지만, UI는 일본어도 지원합니다.
화면은 3분할 되어 있습니다. 왼쪽은 워크스페이스, 중간은 노트북, 오른쪽은 에이전트입니다. (에이전트는 전체 화면도 가능)
탐색은 채팅으로, 결론은 페이지에.
오른쪽 채팅에서 "지난주에 출시한 대시보드, 사용되고 있어?"라고 묻습니다. 에이전트가 aggDSL을 작성하여 Pendo에 던지고, 표를 반환합니다. 여기까지는 일반적인 AI 어시스턴트입니다. 다른 점은 그 답변 아래에 나열된 버튼들입니다.
Add text— 답변의 문장만 노트에 배치
Add DSL as query— 실행된 쿼리를 쿼리 셀(Query Cell)로 배치 (이후부터는 본인의 소유가 되며 편집 가능)
Add table/Add chart/Add both— 결과 표·그래프를 배치
Save as question— 질문 자체를 셀로 배치 (후술)
버튼을 누르기 전까지는 아무것도 노트에 들어가지 않습니다. 이 점이 사소하지만 편리하며, 채팅은 떠오르는 생각을 몇 번이고 질문할 수 있는 공간으로 사용할 수 있습니다. 10번 물어보고 남기는 것은 2번 분량입니다. Pendo의 대시보드 툴이 "먼저 무엇을 볼지 결정한 뒤에 만든다"는 방식이라면, 이것은 대화하면서 좋은 것만 골라 쌓아가는 방식입니다.
| 셀 | 내용 | 재실행 |
|---|---|---|
| Note | Markdown 코멘트 | — |
| Query | 이름을 붙인 aggDSL 쿼리 | Run으로 DSL을 실행하여 자신의 결과 테이블을 업데이트 (LLM 미사용) |
| Result | 반환된 행(Row)의 표. 하나의 쿼리가 소유 | Refresh로 백엔드의 DSL을 재실행 (LLM 미사용) |
| Chart | 꺾은선 / 막대 / 도넛. 다중 계열 가능 | Refresh로 각 계열의 DSL을 재실행 (LLM 미사용) |
| Question | 평문 질문 + 그 답변·표·그래프 | Re-run으로 저장된 DSL을 재생 → LLM이 다시 해석 |
Jupyter Notebook과 마찬가지로, 위에서부터 순서대로 읽을 수 있는 문서이면서 동시에 전부 살아있습니다.
쿼리 셀에는 이름이 붙여지며, 그 이름이 아래 결과 테이블의 헤더(Header)에도 나타납니다. 긴 노트 중간에 있는 표가 "어떤 질문으로부터 나온 숫자인지" 소스(Source)를 확인할 수 있습니다. 이름은 클릭하여 그 자리에서 수정할 수 있으며, 재쿼리 없이 헤더가 따라옵니다.
결과 셀과 차트 셀에는 backing source가 있습니다. 에이전트가 실제로 던진 DSL이 그대로 나오기 때문에, 열어서 수정하고 업데이트할 수 있습니다. 직접 쓰는 것이 번거롭다면 Ask agent to tweak를 통해 채팅으로 돌아가 협상해도 좋습니다.
AI가 작성한 것을 블랙박스로 만들지 않는 것은, 이러한 종류의 툴에서 신뢰를 유지하기 위한 최소한의 조건이라고 생각합니다. 숫자가 이상할 때 쿼리를 보면 알 수 있습니다. 표의 열은 드래그로 정렬할 수 있고, 헤더로 소트(Sort)도 가능합니다.
표의 내용을 읽을 수 있습니다. question cell의 표는 날짜를 사람이 읽을 수 있는 형식으로 변환하고, Pendo의 내부 ID를 실제 Page명·Feature명으로 해결(Resolve)합니다. 저장된 답을 반년 후에 보더라도 의미를 알 수 있도록 설계되어 있습니다.
차트 뒷단의 숫자를 볼 수 있습니다. 에이전트가 만든 요약 차트에는 Show source
버튼이 있어, 그래프와 그 원본 데이터 표를 오갈 수 있습니다. -
노트북 단위의 기본 세그먼트 (Default Segment). 헤더에서 설정하면 셀과 채팅을 포함한 모든 집계 (Aggregation)에 적용됩니다 (쿼리 측에서 덮어쓰지 않는 한). -
워크스페이스 지침 (Workspace Instructions). "요금제는 이렇다", "이 계정은 사내 테넌트이므로 항상 제외"와 같이 매번 쓰고 싶지 않은 내용을 평문으로 적어두는 곳입니다. 매 턴마다 다시 읽히므로, 수정하면 즉시 반영됩니다. -
커스텀 스킬 (Custom Skills). 자신의 어댑션 스코어카드 (Adoption Scorecard), QBR 템플릿, 해지 리스크 감사 등 — 재사용할 플레이북 (Playbook)을 등록해 두면, 질문 내용에 따라 에이전트가 자동으로 해당되는 것을 적용합니다. -
MCP 서버 연결 가능. streamable HTTP를 통해 외부 도구 서버를 연결할 수 있으며, 내장된 Pendo 쿼리와 나란히 에이전트가 호출합니다. -
전체 화면 채팅. 아직 노트에 아무것도 남길 의사가 없는 탐색 단계에서는, 캔버스 전체를 채팅에 할애할 수 있습니다. - 노트는 자동 저장됩니다.
./dev.sh
한 번에, pyenv + venv + Python 의존성 → nvm + Node + Web 의존성 순으로 설치되어 localhost:3000에서 실행됩니다.
Pendo에는 Aggregation API라는 강력한 쿼리 API가 있습니다. 강력하지만, 요청 본문 (Request Body)이 거대한 JSON입니다. pipeline 배열에 source / timeSeries / filter / group / eval / fork … 등이 중첩되어 쌓입니다. JSON이기 때문에 사람이 작성하기도 힘들고, LLM에게 작성하게 하기에는 문서화가 부족하며 (그리고 틀렸을 때 수정하기 어렵습니다).
aggDSL은 그 JSON에 1대1로 대응하는 DSL을 작성한 것입니다. 무엇이 일어나고 있는지 이해하기 쉬우며, 각 연산 (Operation)을 파이프 (Pipe)로 연결하여 작성합니다. 구문은 Kusto에서 영감을 얻어 디자인했습니다.
RESPONSE mimeType=application/json
REQUEST name="DiagEventsAppIdHasData"
FROM event([source=events,blacklist="apply"])
...
이것을 aggdsl compile에 통과시키면, Pendo에 그대로 던질 수 있는 JSON 본문이 나옵니다.
{
"response": { "location": "request", "mimeType": "application/json" },
"request": {
...
DSL로는 327자, JSON으로는 620자. 이것이 그대로 토큰 양에 영향을 미칩니다.
Python으로 파서 (Parser) · 컴파일러 (Compiler) · 디컴파일러 (Decompiler) 3종 세트를 구현했습니다. 디컴파일러가 있기 때문에, 이미 Pendo 측에 있는 aggregation의 JSON을 던져 넣어 DSL로 되돌린 뒤 노트로 가져올 수도 있습니다.
aggdsl compile query.dsl # DSL → Pendo JSON
aggdsl decompile body.json # Pendo JSON → DSL
PES와 같은 단일 명령어도 작성할 수 있습니다.
PIPELINE
| pes {"appId":1234567890123456,"firstDay":"now()","dayCount":-30}
복잡한 예시를 하나 두자면, 페이지 → 기능의 퍼널 분석 (fork / branch / unwind / accumulate를 사용하는 것)은 생(raw) JSON으로는 수백 줄에 달하는 것을 수십 줄의 DSL로 작성할 수 있습니다. examples/에 한 세트 들어있습니다.
구성 요소는 **Nuxt 3 웹 앱 (apps/web) + Python 툴체인 (src/aggdsl, tools/pendo)**입니다.
에이전트에게 전달하는 도구들:
| ツール | 역할 |
|---|---|
lookup_ontology | 제품 맵을 로컬 검색. pendoId와 concept 모두 즉시 반환 (네트워크 불필요) |
lookup_pendo_features / _pages / _segments | 위에서 찾지 못했을 때의 폴백(Pendo API 직접 호출) |
run_pendo_aggregation | aggDSL 문자열과 설명을 받아 실행함 |
build_summary_chart | 여러 aggregation 결과를 하나의 그래프로 요약함 |
run_pendo_aggregation이 호출되면, 서버 측에서는 다음과 같이 처리됩니다.
DSL 문자열
→ python -m aggdsl compile (서브 프로세스, 결과는 캐시)
→ Pendo Aggregation API에 POST
...
이 경로는 셀에서 수동으로 실행하는 것과 완전히 동일합니다. 따라서 채팅에서 본 결과와 노트의 셀 결과는 반드시 같은 형태가 됩니다. '채팅에서는 되는데 셀에서는 다르다'라는 일이 발생하지 않습니다. 서브 프로세스는 세션 단위로 등록되어 있기 때문에, 중간에 멈추면 정말로 멈춥니다.
이 부분이 설계상 가장 큰 판단이었습니다.
Pendo MCP를 사용하면 AI는 '도구를 호출'하는 것만 합니다. 쿼리의 실체는 서버 너머에서 조립됩니다. 올바른 결과가 돌아오기만 하면 되지만, 문제는 실패했을 때입니다.
AI는 왜 실패했는지 알지 못합니다. 자신이 작성하지 않은 것이 실패한 것이므로 고칠 수가 없습니다. 기껏해야 파라미터를 바꿔서 다시 호출하는 정도인데, 그것은 디버깅이 아니라 도박(가챠)에 가깝습니다.
aggDSL 작성을 맡기면, 실패의 모든 책임이 AI 자신에게 돌아옵니다. 그리고 반환되는 에러는 구체적입니다.
// apps/web/server/utils/toolRegistry.ts
const compiled = await compileDsl(dsl, sessionId, { defaultSegmentId: opts.defaultSegmentId ?? null })
if (!compiled.success) return { result: null, error: `DSL compile error: ${compiled.error}` }
...
파서 에러라면 '이 줄의 이 토큰이 부정확', API 에러라면 '이 source에 해당 필드는 없다'. AI의 손에는 자신이 작성한 문자열과 그것에 대한 구체적인 오류가 모두 있습니다. 따라서 수정도 자유롭게 할 수 있습니다.
이것이 self-healing(자가 치유)의 토대입니다. 이 글의 후반부에서 나오는 내용은 전부 이 성질 위에 놓여 있습니다.
- question cell 재실행으로 DSL이 오래되었을 때, 에이전트가
스스로 다시 작성할 수 있는 것은 그 DSL이 자신이 작성한 텍스트이기 때문입니다. - Product map의 concept KPI가 망가졌을 때, 어떤 템플릿이 잘못되었는지 지목할 수 있는 것도 같은 이유입니다. - backing source 패널에 나오는 것이 읽을 수 있는 DSL이므로,
인간 측에서도 검토할 수 있습니다.
MCP가 나쁘다는 이야기가 아니라 (Dendo도 Pendo MCP 서버와 연결 가능합니다), 에이전트가 스스로 고치게 하고 싶은 것은 에이전트 자신이 작성하게 해야 한다는 의미입니다. 블랙박스 너머에서 실패한 것은 누구도 고칠 수 없습니다.
자, 여기까지가 '대화하면서 노트를 만드는' 쪽 이야기였습니다. 이제부터가 v2의 본론으로, 이 대화 그 자체의 질을 어떻게 높였는지에 대한 이야기입니다.
Pendo에는 Leo라는 우수한 AI 어시스턴트가 있습니다. Pendo MCP도 있고. LLM 자체는 똑똑합니다. 그럼에도 불구하고, 실제로 일상적인 분석에서 사용하는 것과 같은 지점에서 막힙니다.
에이전트가 우리 제품이 무엇을 하는지 전혀 모릅니다.
구체적으로는 다음과 같은 일이 발생합니다.
- '활성화(Activation)'라고 물어도, 그것이 어느 Feature의, 어느 기간의, 어느 Segment에 대한 이야기인지
매번 처음부터 추측됩니다. - 이름만 비슷한 다른 Feature를 가져옵니다.Dashboard
검색하여 7건이 반환된 결과 중 그럴듯한 것을 선택합니다. 그것이 틀렸더라도 후속 쿼리는 완벽하게 작동하기 때문에 알아차리기 어렵습니다. - 이전 턴에서 "그 계정은 사내 테넌트니까 제외해줘"라고 말했는데, 다음 세션에서는 잊어버리고 있습니다.
- 비즈니스 목표(Business Goal)를 공유하지 않았기 때문에, 도출되는 시사점이 "일반적인 SaaS의 관행" 수준에서 머뭅니다.
이것은 모델이 똑똑하지 않아서가 아닙니다. 전달하지 않은 것을 알 방법이 없다는 단순한 문제입니다.
그리고 왜 전달하지 못하고 있는가 하면, 측정 도구 안에는 "사실"만 들어있기 때문입니다. Feature, Page, Segment, 이벤트 수, 방문자 수. Pendo가 가지고 있는 것은 여기까지입니다.
"그 Feature가 온보딩(Onboarding)의 완료 지점이다"라거나 "이 Segment는 무료 플랜이므로 매출 목표 대상에서 제외한다"와 같이, 그것이 무엇을 의미하는지는 Pendo 외부에 있습니다. 담당자의 머릿속과 Confluence 어딘가, 그리고 Slack의 과거 로그에 흩어져 있습니다.
그래서 에이전트는 막연하게 추측할 수밖에 없습니다. Pendo 상의 태그나 리포트와 우리의 비즈니스 목표를 연결할 장소가 애초에 어디에도 없었습니다.
그것을 만든 것이 v2입니다.
생각하는 방식은 다른 기사("의미 부여는 나중에 해도 된다")에 자세히 적어두었으니 그쪽도 읽어주시길 권하지만, 핵심은 3개 층으로 나누었다는 것입니다.
┌─────────────────────────────────────────┐
│ 3. 계산층 (Calculation Layer) — usage overlay / concept KPI │ ← 캐시. 그래프에 "칠하기"
├─────────────────────────────────────────┤
...
// apps/web/types/ontology.ts
/**
* Workspace ontology — a lightweight typed graph over the org's Pendo data.
...
"온톨로지(Ontology)"라고 하면 경계심을 가질 수 있으니 미리 써두자면, 이것은 RDF/OWL이 아닙니다. 타입이 지정된 JSON 그래프입니다.
목적이 "추론 엔진을 돌리는 것"이 아니라 "LLM에 문맥(Context)을 전달하는 것"이기 때문입니다. LLM에 전달하려면 결국 전부 프롬프트 문자열이 됩니다. 트리플 스토어(Triple Store)를 구축하고 SPARQL을 작성할 필요는 없으며, 그렇게까지 하면 아무도 유지보수하지 않게 됩니다. 표현력보다는 지속적으로 운영될 수 있는 측면을 선택했습니다.
Sync from Pendo
를 통해 Product Area / Feature / Page / Segment를 가져와서, 힘directed 레이아웃(Force-directed layout) 그래프로 그립니다.
이 층"만"은 수동 편집을 허용하지 않습니다. 이곳은 Pendo의 거울이기 때문입니다. 반드시 Pendo 상에 태그를 만든다 — 라는 역할 분담을 해두었습니다 (의미층의 concept는 수동으로 자유롭게 작성할 수 있습니다. 다음 장).
언제든 재동기화할 수 있으며, 재동기화를 해도 그 위에 올린 의미층은 살아남습니다.
노드 ID는 feature:<pendoId>
와 같이 접두사(Prefix)를 붙여 안정적으로 유지하므로, 재동기화를 해도 concept로부터의 참조가 끊기지 않습니다.
/**
* Structural node. `id` is prefixed and stable across syncs so concept
* references survive re-sync: `feature:<pendoId>`, `page:<id>`,
...
concept는 이름이 붙은 비즈니스 정의입니다. Activation, Power account, Onboarding completion 등입니다.
concept는 수동으로 작성할 수 있으며, 언제든 수정할 수 있습니다. 전용 에디터가 있어 정의와 쿼리, 링크 대상까지 직접 편집할 수 있습니다. "이 Feature와 이 Segment가 나의 관심사다"라고 미리 알려주고 싶다면 그렇게 작성해두면 됩니다.
상단부터 Name / Definition (서술형 정의) / Canonical DSL (저장 시 컴파일 체크 실행) / KPI column (수치 열이 여러 개 반환될 때 어떤 것을 헤드라인 숫자로 쓸지 결정) / Measures (측정할 구조적 노드를 태그로 선택. 우측 상단의 Map with AI
에서 평문으로부터 링크 후보를 추출할 수 있음) / Causes 및 Actions (각각 본문 + 「다음에 물어볼 질문」 템플릿의 2개 열).
※ 이 스크린샷은 합성 데모 워크스페이스(가상의 제품 "Acme Workspace", 가상의 기능명·수치)에서 촬영된 것이며, 실제 데이터는 전혀 포함되어 있지 않습니다.
AI에게 초안을 작성하게 하는 단계는 있지만, 저장되는 것은 사람이 확인한 후입니다.
즉, 이곳은 인간의 영역으로 남겨두었습니다. 후반부에 기술할 「사용하면서 익혀가는」 메커니즘은, 이 수기 작성을 대체하는 것이 아니라, 방치하면 놓치게 될 부분을 보완하기 위한 것입니다.
export interface OntologyConcept {
id: string
name: string
...
definition: 무엇을 측정할 것인지, 어떤 기간 동안, 누구를 대상으로 하는지. 산문 형태로 작성.
dslTemplate: 그것을 측정하기 위한 정전(canonical) 쿼리. 저장 시 컴파일 체크가 실행됨.
measures: 어떤 Feature/Page/Segment로부터 계산되는지, 구조 계층(structural layer)으로의 실제 링크.
causes / actions: 숫자가 변한 이유에 대한 가설과 대응책. 각각에 「다음에 물어봐야 할 질문」 템플릿이 붙음.
이것이 시스템 프롬프트(System Prompt)에 주입됩니다. 「활성화(Activation) 상태는 어때?」라고 물었을 때, 에이전트는 정의를 다시 추측하는 것이 아니라, 우리가 결정한 정의와 쿼리를 사용합니다. 동일한 지표가 매번 동일한 숫자로 반환됩니다.
digest를 구성하는 코드에서 가장 효과적이었던 부분은 여기입니다.
// apps/web/server/utils/ontologyDigest.ts
// pendoIds are usable directly in DSL (featureId=[...] etc.) — emitting
// them here is what lets the agent skip live lookup round-trips.
...
온톨로지(Ontology)가 없는 상태의 에이전트는 매 턴 다음과 같은 과정을 거칩니다.
lookup_pendo_features("dashboard")를 호출함 - 유사한 이름이 7개 반환됨
그중 하나를 선택함 ←
서두에 언급한 「짐작하기」가 바로 이 단계 - DSL을 구성함
실행함
digest에 pendoId를 적어두면 1~3단계가 통째로 사라집니다. 잘못 짚을 여지 자체가 없어집니다.
에이전트의 UX를 개선한다는 것은, 더 똑똑한 모델로 교체하는 것보다 먼저 "애초에 추측하게 만들지 않는 것"이었습니다. 왕복(round-trip)이 줄어들기 때문에 빨라지고, 저렴해지며, 무엇보다 정확해집니다.
digest에는 다음과 같은 사용 지침도 포함되어 있습니다.
- When the question matches a concept by name or meaning, START from its DSL
template — adapt only the time window / segment / grouping. Do not re-derive
the query from scratch.
...
Dendo의 시스템 프롬프트는 3개 계층(Layer)으로 구성됩니다.
Layer 1: aggDSL 명세서, 워크플로우 스킬, 세션 리플레이(Session Replay) 예시. 프로세스 수명 동안 캐싱하여 프롬프트 캐시(Prompt Cache)에 올림.
Layer 2: 노트북의 문맥 등 매 턴 변하는 것.
Layer 3: 온톨로지 digest, 커스텀 스킬, 워크스페이스 지침. 캐싱하지 않음.
온톨로지를 Layer 3에 배치한 이유는, concept를 수정한 즉시 다음 턴부터 바로 적용되기를 원했기 때문입니다. 캐시 효율보다 그쪽을 우선시했습니다. 정의를 수정했는데 반영이 한 턴 늦어지는 도구라면, 아마 아무도 정의를 수정하려 하지 않을 것입니다.
글자 수 상한도 설정해 두었습니다.
/** Hard cap so the digest can never crowd out the rest of Layer 3. */
const MAX_DIGEST_CHARS = 6000
concept 단위로만 분할합니다.
// 전체 컨셉 예산 편성(Whole-concept budgeting): 블록 중간을 절대 자르지 않습니다 (이전의 맹목적인 tail-slice 방식은
// DSL 코드 펜스 내부를 잘라버릴 수 있었습니다). 넘치는 컨셉은
// 대신 lookup_ontology를 통해 접근할 수 있습니다.
정의만 적혀 있는 사전은 아무도 보러 오지 않습니다. 그래서 각 컨셉(concept)은 KPI와 스파크라인(sparkline), 그리고 전 기간 대비 비율을 가집니다. dslTemplate이 있다면 그것을 사용하고, 없다면 measures로부터 자동 생성한 쿼리로 계산합니다.
그리고 어느 쪽으로 계산했는지를 레이블에 기록하도록 했습니다.
export interface ConceptMetric {
value: number
/** `value`가 무엇인지 — 추출 휴리스틱(extraction heuristic)을 정직하게 인코딩합니다. 예:
...
Σ accounts (4 rows)와 같이 "이 숫자가 무엇인지"를 반드시 출력합니다. 숫자를 그것이 실제로 나타내지 않는 다른 것으로 오인하게 만들지 않는 것은, 이런 종류의 도구에서 가장 중요한 정직함이라고 생각합니다.
에러 또한 컨셉 단위로 격리해 두었습니다. dslTemplate 하나가 고장 나더라도 해당 컨셉이 "KPI unavailable"이 될 뿐, 지도 전체가 무너지지는 않습니다.
이 부분이 이번에 가장 만들고 싶었던 부분입니다.
전제로, 앞서 썼듯이 컨셉은 직접 작성할 수 있습니다. 관심 있는 태그를 먼저 등록해 두는 것이 빠르다면, 그렇게 하는 것이 정답입니다.
하지만, 처음에 전부 다 써내려고 하면 실패합니다. 모든 태그에 의미를 부여하는 프로젝트는 대개 설계한 사람이 부서를 이동하는 시점에 사장됩니다. 그리고 실제로 운용해 보면, 나중에 힘을 발휘하는 지식은 "직접 해보며 알게 된 것" 쪽에 있습니다. "이 지표가 떨어질 때는 대개 월초의 임포트(import)가 막혀 있다"와 같은 것은 사전 설계 단계에서는 절대 나올 수 없습니다.
그래서 두 가지 방식을 모두 준비했습니다. 먼저 작성해도 되고, 사용되면서 성장해도 됩니다. 다음은 후자에 대한 이야기입니다.
conceptEvolution.ts가 하고 있는 일입니다. 에이전트가 실제 데이터로 질문에 답한 후, 그 대화에서 언급된 기존 컨셉만 추가 및 업데이트합니다.
/**
* 컨셉 자가 유지보수(Concept self-maintenance): 에이전트가 실제 Pendo
* 데이터를 사용하여 질문에 답변한 후, 해당 질문이 다룬 기존(EXISTING) 컨셉을
* 풍부하게 만듭니다 — 오직 추가(additive)만 가능합니다
...
네 가지 가드레일(guardrail)을 설치했습니다.
① 결정론적 게이트(Deterministic gate)를 먼저 통과시킨다. 매칭되지 않으면 LLM을 호출하지 않는다
컨셉의 이름이 대화에 나왔는지, 컨셉이 측정하고 있는 엔티티(entity) 이름이 나왔는지, 지도의 어느 노드에서 질문이 시작되었는지. 이를 문자열 매칭으로 판정하여 후보가 zero라면 거기서 끝냅니다.
if (candidates.length === 0) return []
"모든 대화를 LLM에 읽게 하여 온톨로지(ontology)를 업데이트하게 하는 것"은 비용과 정확도 측면에서 모두 파산합니다. 저렴한 필터로 대부분을 걸러낸 뒤에 높은 성능의 모델을 호출합니다.
② 추가만 허용한다. 이름, 정의, 템플릿은 절대로 건드리지 못하게 한다
사람이 합의한 정의를 AI가 바꿔버린다면, 그것은 더 이상 공유 어휘가 아닙니다. 늘릴 수 있는 것은 measures / causes / actions 뿐입니다.
③ "아무것도 더하지 않는 것이 보통은 정답이다"라고 프롬프트에 명시한다
- 대부분의 교환에서 아무것도 추가하지 않는 것(Adding NOTHING)이 정답입니다 — 일반적인 조회(lookup)는 온톨로지에 아무것도 가르쳐주지 않습니다. 워크스페이스가 재사용할 수 있는 지속 가능한 지식만을 추가하세요.
- 만약 기존의 원인(cause)이나 조치(action)가 이미 동일한 테마나 권장 사항을 다루고 있다면 —
...
LLM은 그대로 두면 반드시 무언가를 추가합니다. "오늘은 배울 내용이 없었습니다"라고 말하게 하려면 명시적으로 지시할 필요가 있었습니다. 이것을 작성하기 전까지, 온톨로지 (Ontology)는 일주일 만에 노이즈의 산이 되어 있었습니다.
④ 연결 가능한 엔티티(Entity)를 해당 대화에 등장한 것으로만 제한하기
환각 (Hallucination) 방지 대책입니다. 모델에게 "이 ID 리스트에서만 선택하라"고 전달합니다. 게다가 그 후보 리스트 자체를 해당 대화에서 실제로 이름이 언급된 노드(Node)로만 구성합니다.
// Entities the model may link as new measures: only ones this exchange
// actually names, so a hallucinated link is impossible. Single-word names
// ("Guides", "Features") match the prose of almost any answer — those only
...
Guides
Features
와 같은 단일 단어 형태의 기능(Feature) 명칭은 어떤 답변의 산문(Prose)과도 우연히 일치할 수 있습니다. 따라서 단일 단어 이름은 사용자의 질문문에 등장했을 때만 후보에 포함합니다. 여러 단어로 구성된 이름이라면 대화 중 어디에서든 등장하면 실제 존재하는 것으로 간주합니다. 이 비대칭적인 규칙에 도달하기까지 꽤 많은 오연결(Mislink)을 겪었습니다.
다시 쓰는(Write-back) 측에서도 중복 판정, 상한 클램핑 (Upper clamp), 실제 노드 ID 검증을 다시 한번 수행합니다. LLM의 출력을 그대로 신뢰하는 곳을 단 한 군데도 만들지 않습니다.
/** Paraphrase-tolerant duplicate test: exact or substring either way. */
function isDuplicate(candidate: string, existing: string[]): boolean {
const c = norm(candidate)
...
신규 컨셉 (Concept) 생성은 자동화하지 않았습니다. 이는 의도적인 것입니다. 온톨로지의 가치는 망라성이 아니라 정확도에 있으므로, 늘어나는 지점에 사람의 승인을 거치도록 합니다.
대신 Suggest가 제안만 합니다. 증거원은 두 가지입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기