Meta發表100公克VR眼鏡,支援虛擬多螢幕與AI操作
iThome 新聞
Meta周三(9/23)發表新一代虛擬實境眼鏡Meta VR Glasses,將原本笨重的VR頭戴裝置縮小成約100公克的眼鏡,讓使用者可隨時在眼前開啟虛擬電影院或工作空間。新產品預計2027年春季上市,售價1,299.99美元。
Score: 55.81Confidence: 54%
View offerLoading the catalog…
THE AI OPPORTUNITY INDEX
Find your next AI tool. Explore free access, trials, and credits — all in one place.
iThome 新聞
Meta周三(9/23)發表新一代虛擬實境眼鏡Meta VR Glasses,將原本笨重的VR頭戴裝置縮小成約100公克的眼鏡,讓使用者可隨時在眼前開啟虛擬電影院或工作空間。新產品預計2027年春季上市,售價1,299.99美元。
Score: 55.81Confidence: 54%
View offeriThome 新聞
GitLab將於10月19日起調整GitLab.com流量限制,Free方案與未經驗證的請求率先套用新規則,Premium與Ultimate方案預定2027年1月跟進。未經驗證的一般網頁與API請求,單一IP位址每小時最多60次,GitLab表示,GitLab.com需求快速成長,今年平臺負載預期增加數倍,因此需要調整流量限制,因應包含自動化工具與AI代理在內的各類工作負載。
Score: 55.81Confidence: 54%
View offerInfoQ - 促进软件开发领域知识与创新的传播
点击查看原文>
Score: 55.79Confidence: 54%
View offervelog
본 문서는 Claude를 사용해 정리했습니다. 『실무로 통하는 타입스크립트』 특정 프로퍼티만 선택형으로 만들기 목표 : 일부 프로퍼티만 선택형으로 바꾼 타입을 파생한다. 접근 : 두 객체 타입의 인터섹션 을 만드는 헬퍼 타입을 정의한다. 한 객체는 선택된 키를 선택형( ? )으로 매핑하고, 다른 객체는 나머지 프로퍼티를 그대로 매핑한다. type Person = { name: string; age: number; profession: string; }; // Partial과 달리 지정한 키만 선택형으로 매핑한다 type SelectPartial<T, K extends keyof T> = { [P in K]?: T[P]; }; type Age = SelectPartial<Person, "age">; 나머지 키와 인터섹션 type ExcludeAge = Exclude<"name" | "age", "age">; // "name" type SetOptional<T, K extends keyof T> = { [P in K]?: T[P]; } & { [P in Exclude<keyof T, K>]: T[P]; }; type OptionalAge = SetOptional<Person, "age">; type OptionalAgeAndProf = SetOptional<Person, "age" | "profession">; Exclude<keyof T, K> 로 선택된 키를 뺀 나머지를 구한 뒤, 두 매핑 타입을 & 로 합친다. OptionalAge 에서는 name 과 profession 이 필수, age 만 선택형이다. 내장 헬퍼 타입만으로도 구현할 수 있다 type SetOptional2<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K>; type OptionalAge2 = SetOptional2<Person, "age">; 더 짧지만, 결과가 인터섹션 형태라 실제 프로퍼티가 무엇인지 에디터에서 바로 보이지 않고, 어떻게 만들어졌는지만 드러난다. Remap으로 평평하게 만들고, 기본값으로 편의성 높이기 type Remap<T> = { [K in keyof T]: T[K]; }; type SetOptional3<T, K extends keyof T = keyof T> = Remap Partial<Pick<T, K>> & Omit<T, K> >; SetOptional2 SetOptional3 K 기본값 없음 (생략 불가) keyof T K 생략 시 타입 에러 모든 프로퍼티가 선택형 결과 형태 인터섹션 그대로 Remap 으로 하나의 평평한 객체 타입 type AllOptional = SetOptional3<Person>; // 전부 선택형 — {} 도 통과 type OnlyAgeOptional = SetOptional3<Person, "age">; // age만 선택형, name은 여전히 필수 Remap 은 인터섹션을 하나의 객체 타입으로 풀어서, 에디터 툴팁에 실제 프로퍼티가 나열되게 한다. 반대 방향 — SetRequired, OnlyRequired // 일부 키를 꼭 필요하게 type SetRequired<T, K extends keyof T = keyof T> = Remap Required<Pick<T, K>> & Omit<T, K> >; // 제공한 키는 전부 필수, 나머지는 선택형 type OnlyRequired<T, K extends keyof T = keyof T> = Remap Required<Pick<T, K>> & Partial<Omit<T, K>> >; 중첩 객체까지 처리하는 재귀 헬퍼 타입 Partial , Required , Readonly 같은 내장 헬퍼는 객체의 첫 번째 수준만 바꾼다. 중첩된 객체 프로퍼티는 처리하지 못한다. 해결 : 같은 동작을 중첩된 객체에도 반복하는 재귀 헬퍼 타입 을 만든다. type Settings = { mode: "light" | "dark"; playbackSpeed: number; subtitle: { active: boolean; color: string; }; }; const defaults: Settings = { mode: "dark", playbackSpeed: 1.0, subtitle: { active: false, color: "white" }, }; function applySettings( defaultSettings: Settings, userSettings: Partial<Settings>, ): Settings { return { ...defaultSettings, ...userSettings }; } // Partial은 첫 수준에서만 동작한다 — subtitle 안의 color가 빠졌다고 에러 let settings = applySettings(defaults, { subtitle: { active: true } }); TS2741: Property 'color' is missing in type '{ active: true; }' but required in type '{ active: boolean; color: string; }'. DeepPartial type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]>; }; 원시 타입이 T 로 들어와도 에러가 나지 않는다. [K in keyof T]?: 형태의 매핑 타입은 원시 타입을 그대로 통과시켜서, DeepPartial<string> 은 그냥 string 이 된다. // 객체일 때만 깊숙이 들어가도록 조건을 명시한 버전 type DeepPartial2<T> = T extends object ? { [K in keyof T]?: DeepPartial2<T[K]> } : T; type DeepPartialSettings = DeepPartial2<Settings>; 함수 프로퍼티, 배열, string | 객체 유니온, null 네 경우에서 DeepPartial 과 DeepPartial2 의 차이를 재현하지 못했다. 조건 분기는 "원시 타입은 그대로 둔다"는 의도를 코드에 드러내는 표기로 보인다. 스프레드로 합치면 여전히 에러가 난다 function applySettings( defaultSettings: Settings, userSettings: DeepPartial2<Settings>, ): Settings { return { ...defaultSettings, ...userSettings }; // 에러 } Type '{ ...; subtitle: { active?: boolean | undefined; color?: string | undefined; }; }' is not assignable to type 'Settings'. Type 'boolean | undefined' is not assignable to type 'boolean'. defaultSettings 가 Settings 를 만족하니 결과도 괜찮을 것 같지만 그렇지 않다. 스프레드는 얕은 병합이라 subtitle 이 통째로 덮어씌워지기 때문이다. 결과의 subtitle 타입은 userSettings.subtitle 의 타입( active? , color? 가 모두 선택형)이 되어 Settings 의 요구( active: boolean )를 만족하지 못한다. 런타임에서도 같은 문제가 그대로 드러난다. { ...defaults, ...{ subtitle: { active: true } } } // subtitle: { active: true } ← color가 사라짐 lodash의 merge로 해결 기능 구현이 까다로우므로 lodash의 merge 를 이용한다. merge 는 두 객체의 인터섹션을 반환하도록 타입이 정의되어 있다. // @types/lodash merge<TObject, TSource>(object: TObject, source: TSource): TObject & TSource; import { merge } from "lodash"; function applySettings2( defaultSettings: Settings, userSettings: DeepPartial2<Settings>, ): Settings { return merge(defaultSettings, userSettings); } Settings & DeepPartial2<Settings> 는 Settings 에 할당 가능하므로 타입이 통과한다. 런타임에서도 깊은 병합 이 일어난다. merge(defaults, { subtitle: { active: true } }) // subtitle: { active: true, color: "white" } ← color가 살아 있음 비슷한 방법으로 DeepOnly , DeepRequire 도 만들 수 있다.
velog
"추석 연휴 전 마지막 스퍼트, 벤치마크까지 돌린 한 주" 📍 FACTS (사실) 최종 프로젝트 월(9/21) — 휴가 화(9/22) — 깃 변경분 병합, 한글 파일 생성, 강사님 미팅 문서 작성, 입력값 저장 테이블 생성 및 연결, 버그 수정, 비동기화 작업, 신규 필드 대량 추가, 검증결과서 연동, HWP 본문 채우기, plan_canonical_data 신규 테이블 생성 수(9/23) — 웹 서비스 로그 파일 생성, 스몰 회의, 5~9B 한국어 기반 모델 성능 비교표 작성, EXAONE-3.5-7.8B-Instruct 벤치마크 (4비트 양자화, 20개 샘플 추론), 모델 선정 완료 목(9/24)~일(9/28) — 추석 연휴 코딩테스트 9/21 — 없음 (휴가) 9/22 — 없음 9/23 — 글자 이어 붙여 문자열 만들기 💬 FEELINGS (느낌) 이번 주는 화요일과 수요일 이틀에 엄청난 양의 작업을 몰아서 했다. 신규 필드 대량 추가, HWP 본문 채우기, 비동기화까지 할 일 목록이 어마어마했는데 하나씩 처리하면서 뿌듯함을 느꼈다. 특히 LLM 벤치마크를 직접 돌려보면서 이론으로만 알던 양자화, VRAM 사용량, 토큰 생성 속도 등이 숫자로 와닿아서 흥미로웠다. 트러블슈팅을 여러 번 겪으면서 힘들기도 했지만 결국 다 해결하고 모델 선정까지 완료했다. 추석 연휴가 기다리고 있어서 조금만 더 힘내자는 마음으로 버텼다. 🔍 FINDINGS (배운 것) 기술적으로 배운 것 LLM 모델 벤치마크 — 양자화(4-bit), VRAM 사용량, 토큰 생성 속도를 직접 측정하고 비교해보면서 모델 선택 기준이 성능뿐만 아니라 속도와 메모리 효율도 중요하다는 것을 배움 다양한 트러블슈팅 경험 — create_causal_mask 호환성 이슈, OOM 오류 등을 직접 해결하면서 실력이 쌓임 협의가 필요한 부분과 혼자 해결할 수 있는 부분을 명확히 구분하고 혼자 할 수 있는 것부터 처리하는 것이 효율적이라는 것을 배움 🏃 FUTURE (미래) 추석 연휴 동안 푹 쉬기 🎑 💌 나에게 보내는 응원 이틀 만에 어마어마한 양의 작업을 해냈어. 벤치마크도 돌리고, 트러블슈팅도 해결하고, 모델 선정까지 정말 알찬 한 주였어. 추석 연휴 동안 푹 쉬고, 충전해서 돌아오자. 수고했어! 🎑✨
Score: 54.4Confidence: 49%
View offervelog
「일부만 안 된다」는 신고와 「건수 + 1」의 함정 (2026.09.19 ~ 09.22) 이번 주는 점검 요청 하나와 고객 신고 둘이 겹쳤다. 셋 다 "이미 알고 있다고 생각한 것"이 틀렸다는 공통점이 있어서 같이 적어 둔다. 점검은 차집합으로 한다 커머스 플랫폼 쪽에서 "새로 승인된 제휴가 있는지 확인해 달라"는 요청이 왔다. 새 승인은 없었다. 거기서 끝낼 수도 있었는데, 승인 목록과 실제 사이트 연결 목록을 나란히 놓고 보니 이미 승인된 매장 몇 곳이 여러 사이트 중 한 곳에만 연결돼 있었다. 나머지 사이트에서는 승인을 받아 놓고도 목록형으로 놀고 있었던 셈이다. 승인이 날 때마다 사이트 수만큼 관리자 폼을 반복해서 누르는 일은 사람이 할 일이 아니라고 판단했다. 관리자 화면과 같은 검증·저장·게시 경로를 타는 내부 일괄 입구를 만들어 미연결 십여 쌍을 한 번에 넣었다. 다음 자동 연결 주기에서 사이트마다 수십 건이 제휴형으로 붙었고 검색 노출에서 빠져 있던 매장들도 색인 대상이 됐다. 수익이 얼마나 늘었는지는 아직 집계 전이라 숫자가 없다. "새 승인 없음"은 질문에 대한 답이지 점검의 끝이 아니다. 가진 것과 쓰고 있는 것의 차집합을 구해야 놀고 있는 자산이 보인다. 실수도 하나 있었다. 로컬 브랜치가 서버보다 뒤처진 상태에서 배포 스크립트를 돌려 옛 코드를 다시 배포했다. 다행히 무해했지만 공유 트리에서는 푸시 전에 behind 를 세고 뒤면 임시 워크트리에서 작업하기로 했다. 표시용 문자열에서 숫자를 다시 뽑지 않는다 레거시 관리 시스템에서 「1TB 모델 전 색상에 바이어 단가가 붙지 않는다」는 신고가 왔다. 256GB·512GB 는 되고 1TB·2TB 만 안 됐다. 표기와 계산값이 따로 놀았다. 서버는 1024GB 를 화면용으로 「1T」라고 내려주는데, 화면은 이 문자열에서 숫자만 뽑아 「1」로 단가표 키를 만들고 있었다. 단가표는 1024 로 저장돼 있으니 영원히 맞을 수 없다. 같은 방식이 여섯 곳에 흩어져 있었고 저장 쪽도 같은 버그라 「용량 1」짜리 잘못된 행을 단가표에 만들어 두고 있었다. 그 행 때문에 일부 색상이 엉뚱한 단가를 받은 것도 이때 확인했다. 서버와 같은 변환 규칙 하나로 여섯 곳을 통일하고 0원 단가는 「단가 없음」으로 드러나게 했다. 48행 전표에서 48행 전부 단가가 적용됐다. 고치기 전이라면 18행은 빈칸, 27행은 엉뚱한 값이었을 것이다. 뼈아픈 건 이 버그를 3일 전 내가 옮겨 심었다는 점이다. 등록 창 코드를 수정 창으로 복사하면서 같이 가져왔다. 이전 세대 모델의 1TB 도 처음부터 안 됐는데 아무도 몰랐다. 앞으로 「일부 값만 안 된다」는 신고는 표기와 계산값이 갈라지는 지점부터 본다. 표시용 문자열에서 값을 역산하지 말고 원래 값을 쓴다. 코드를 복사해 옮길 때는 그 코드의 버그도 같이 옮긴다는 걸 전제로 한 번 더 본다. 일련번호는 최대값 기준으로 만든다 같은 시스템에서 출고 등록 중 unique 제약 위반으로 저장이 안 된다는 신고가 왔다. 한 번 나면 그날 내내 모든 등록이 막히는 문제였다. 그날 번호를 뽑아 보니 중간 하나가 지워져 있었다. 번호 생성이 「그날 건수 + 1」이라, 삭제가 한 건 있으면 이미 쓴 번호를 다시 발급한다. 「그날 최대 번호 + 1」로 바꿨다. 다만 창을 열 때 번호가 정해지는 구조라 동시 등록에서도 겹칠 수 있다고 봤다. 저장 시점에 다음 번호로 최대 5번 재시도하게 했다. 재시도로 바뀐 번호는 감사로그에 남기고 사람이 직접 적은 번호는 말없이 바꾸지 않게 했다. 구멍 상태에서 옛 규칙이 중복 번호를 내던 것이 새 규칙에서는 다음 번호로 나왔다. 저장 직전 다른 사람이 같은 번호를 선점하는 경쟁 상황도 재현해 자동으로 다음 번호에 저장되고 감사로그에 남는 것을 확인했다. 신고에서 운영 반영까지 같은 날 오전에 끝났다. 생성기만 고치면 절반이다. 번호가 확정되는 시점에 충돌 대응까지 있어야 한다. 그리고 DB 의 unique 제약이 조용한 중복 데이터를 막아 준 마지막 방어선이었다. 오류로 드러난 게 오히려 다행이었다.
Score: 54.4Confidence: 49%
View offervelog
오늘의 프로젝트 개발 -> 링크 적 캐릭터의 공격 스킬 추가 적 캐릭터의 행동 패턴 구현
Score: 54.4Confidence: 49%
View offervelog
🌱 들어가며 요즘 Claude, ChatGPT, Cursor 같은 AI 도구를 쓰다 보면 LLM , MCP 같은 단어를 정말 자주 보게 된다. 막연하게 "AI 모델이구나", "뭔가 연결하는 거구나" 정도로만 알고 있었는데, 백엔드 개발자로서 이걸 내 서비스에 어떻게 붙일 수 있는지 제대로 알고 싶어서 직접 공부하고 만들어 봤다. 이번 글에서는 LLM이 무엇이고 어떻게 동작하는지 LLM의 한계와, 그걸 보완하는 MCP가 무엇인지 Spring AI 로 간단한 MCP 서버를 만들어서 AI 클라이언트에 연결하는 과정 까지 정리해 본다. 🧠 LLM이란? LLM(Large Language Model, 대규모 언어 모델) 은 방대한 양의 텍스트를 학습해서 사람처럼 글을 이해하고 생성하는 모델 이다. ChatGPT의 GPT, Anthropic의 Claude, Google의 Gemini 등이 모두 LLM이다. 동작 원리: "다음 단어 맞히기" LLM의 원리는 생각보다 단순하다. 주어진 문장 다음에 올 가장 그럴듯한 단어를 예측 하는 것을 반복한다. 입력: "스프링에서 의존성을 주입하는 방법은" ↓ 다음 단어 예측 "생성자" → "주입," → "필드" → "주입," → ... 인터넷의 수많은 문서, 코드, 책을 학습하면서 "이런 문맥에서는 이런 말이 나온다"는 패턴을 익혔기 때문에, 단어 예측만 반복하는데도 질문에 답하고 코드를 짜는 것처럼 보이는 것이다. 알아두면 좋은 용어 용어 설명 토큰(Token) LLM이 텍스트를 처리하는 단위. 단어보다 작은 조각이며, 한글은 보통 한 글자가 1~2 토큰 정도다. API 비용도 토큰 단위로 계산 된다. 컨텍스트 윈도우 LLM이 한 번에 볼 수 있는 입력 + 출력의 최대 토큰 수. 대화가 길어지면 앞부분을 잊는 이유다. 프롬프트(Prompt) LLM에게 주는 입력. 역할을 지정하는 시스템 프롬프트 와 실제 질문인 유저 프롬프트 로 나뉜다. 환각(Hallucination) 모르는 내용을 그럴듯하게 지어내는 현상. "다음에 올 그럴듯한 말"을 만드는 구조상 생기는 문제다. 학습 기준일(Cutoff) 모델이 학습한 데이터의 마지막 시점. 그 이후의 정보는 모른다. LLM의 한계 LLM은 똑똑하지만, 기본적으로 학습된 지식 안에서만 답할 수 있다. ❌ 내 서비스 DB 에 있는 데이터를 모른다 ❌ 오늘 날씨, 최신 뉴스 같은 실시간 정보를 모른다 ❌ 직접 API를 호출하거나 파일을 수정 하는 등 행동을 할 수 없다 그래서 등장한 개념이 Tool Calling(Function Calling) 이다. LLM에게 "이런 함수들이 있어"라고 알려주면, LLM이 필요할 때 "이 함수를 이 파라미터로 호출해줘" 라고 요청하고, 애플리케이션이 실제로 실행한 결과를 다시 LLM에게 돌려준다. 사용자: "최근 7일 결제 장애 있었어?" ↓ LLM: (내가 모르는 정보네 → search-incidents(service="결제", days=7) 호출 요청) ↓ 애플리케이션: 실제 DB 조회 → 결과 반환 ↓ LLM: "네, 3일 전에 PG사 타임아웃으로 인한 장애가 1건 있었습니다." 그리고 이 Tool Calling을 표준화 한 것이 바로 MCP다. 💡 MCP란? MCP(Model Context Protocol) 는 Anthropic이 2024년 11월에 공개한 LLM과 외부 도구/데이터를 연결하기 위한 표준 프로토콜 이다. 흔히 "AI를 위한 USB-C" 라고 비유한다. MCP 이전: AI 앱마다 도구 연동 방식이 제각각 → 도구 N개 × AI 앱 M개 = N×M개의 연동 코드 MCP 이후: 도구는 MCP 서버로 한 번만 만들고, MCP를 지원하는 모든 AI 앱에서 사용 → N+M ┌────────────────────┐ ┌─────────────────────┐ │ MCP Host │ │ MCP Server (내가 │ │ (Claude, Cursor, │ JSON- │ 만들 서버) │ │ IDE 등) │ RPC │ - Tools │ │ └ MCP Client ────┼─────────▶│ - Resources │──▶ DB / API │ │◀─────────┼ - Prompts │ └────────────────────┘ └─────────────────────┘ MCP 서버가 제공하는 3가지 구분 설명 예시 Tools LLM이 호출 할 수 있는 함수 주문 조회, 티켓 생성 Resources LLM이 읽을 수 있는 데이터 설정 파일, 문서 Prompts 재사용 가능한 프롬프트 템플릿 "코드 리뷰해줘" 템플릿 이 중 가장 많이 쓰이는 건 Tools 다. 이번 글도 Tool 위주로 진행한다. 🤔 Function Calling이랑 뭐가 다른데? LLM이 함수를 호출한다는 개념 자체는 같다. 차이는 어디에 붙어있느냐 다. Function Calling : 내 애플리케이션 코드 안에서 특정 LLM API에 함수를 등록 → 내 앱에서만 사용 가능 MCP : 함수를 독립된 서버 로 분리하고 표준 프로토콜로 노출 → Claude, Cursor, 다른 에이전트 등 어디서든 재사용 즉, MCP는 Function Calling을 표준화하고 재사용 가능하게 만든 것 이라고 이해하면 된다. 백엔드 개발자 입장에서 보면 "AI가 호출하는 API 서버" 를 만드는 것과 비슷하다. 🔑 Spring AI로 MCP 서버 만들기 Java 진영에서는 Spring AI 가 MCP 서버/클라이언트를 모두 지원한다. 특히 Spring AI 2.0부터는 @McpTool 어노테이션이 Spring AI 본체로 들어오면서, 컨트롤러 만들듯이 MCP 도구를 만들 수 있게 됐다. 이 글은 Spring AI 2.0.x + Spring Boot 4.x 기준이다. 예제 시나리오 토이 프로젝트용으로 서비스 장애 이력 을 저장하는 간단한 서버를 만들고, 이 조회 기능을 MCP로 노출한다. AI에게 "지난주에 결제 관련 장애 있었어?" 라고 물어보면 AI가 직접 조회해서 답하게 만드는 것이 목표다. 🧲 환경 구성 1. build.gradle dependencies { implementation platform("org.springframework.ai:spring-ai-bom:2.0.1") // MCP 서버 (Streamable HTTP, Spring MVC 기반) implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'com.h2database:h2' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' } 전송 방식에 따라 스타터가 나뉜다. 전송 방식 스타터 용도 STDIO spring-ai-starter-mcp-server 로컬 프로세스로 실행 HTTP (WebMVC) spring-ai-starter-mcp-server-webmvc 서버로 띄워서 원격으로 사용 HTTP (WebFlux) spring-ai-starter-mcp-server-webflux 리액티브 환경 익숙한 Spring MVC 방식으로 서버를 띄우고 싶어서 WebMVC + Streamable HTTP 를 선택했다. 2. application.yml spring: ai: mcp: server: name: incident-mcp-server version: 1.0.0 type: SYNC protocol: STREAMABLE # Streamable HTTP annotation-scanner: enabled: true # @McpTool 자동 스캔 이렇게만 설정하면 POST /mcp 엔드포인트가 자동으로 열린다. 🛠️ Tool 구현 Entity & Repository @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Incident { @Id @GeneratedValue private Long id; private String title; private String service; // 결제, 회원, 주문 ... @Enumerated(EnumType.STRING) private Severity severity; private String cause; private LocalDateTime occurredAt; } public enum Severity { CRITICAL, MAJOR, MINOR } public interface IncidentRepository extends JpaRepository<Incident, Long> { List<Incident> findByServiceAndOccurredAtAfterOrderByOccurredAtDesc( String service, LocalDateTime from); List<Incident> findTop10ByOrderByOccurredAtDesc(); } MCP Tool import org.springframework.ai.mcp.annotation.McpTool; import org.springframework.ai.mcp.annotation.McpToolParam; @Component @RequiredArgsConstructor public class IncidentTools { private final IncidentRepository incidentRepository; @McpTool( name = "search-incidents", description = "특정 서비스의 최근 장애 이력을 조회한다. 서비스명과 조회할 기간(일)을 받는다.", annotations = @McpTool.McpAnnotations(readOnlyHint = true) ) public List<IncidentDto> searchIncidents( @McpToolParam(description = "서비스명 (예: 결제, 회원, 주문)", required = true) String service, @McpToolParam(description = "오늘로부터 며칠 전까지 조회할지", required = true) int days) { LocalDateTime from = LocalDateTime.now().minusDays(days); return incidentRepository .findByServiceAndOccurredAtAfterOrderByOccurredAtDesc(service, from) .stream() .map(IncidentDto::from) .toList(); } @McpTool( name = "recent-incidents", description = "전체 서비스의 최근 장애 10건을 조회한다.", annotations = @McpTool.McpAnnotations(readOnlyHint = true) ) public List<IncidentDto> recentIncidents() { return incidentRepository.findTop10ByOrderByOccurredAtDesc() .stream() .map(IncidentDto::from) .toList(); } } public record IncidentDto( Long id, String title, String service, String severity, String cause, LocalDateTime occurredAt) { public static IncidentDto from(Incident i) { return new IncidentDto(i.getId(), i.getTitle(), i.getService(), i.getSeverity().name(), i.getCause(), i.getOccurredAt()); } } @RestController 에 @GetMapping 붙이던 것과 거의 비슷하다. 차이점은 딱 하나, 💡 description 이 곧 API 문서이자 프롬프트다. 앞에서 본 것처럼 LLM은 결국 텍스트를 보고 판단 한다. 그래서 description 을 읽고 "이 도구를 언제, 어떤 파라미터로 호출할지" 를 결정한다. 설명이 모호하면 AI가 엉뚱한 도구를 호출하거나 아예 호출하지 않는다. Swagger 문서 쓰듯이 구체적으로 작성하는 것이 핵심이다. 또 반환값은 Spring AI가 JSON으로 직렬화해서 LLM에게 전달한다. 반환 데이터도 결국 토큰 이기 때문에, 엔티티를 그대로 반환하기보다는 필요한 필드만 담은 DTO 를 반환해야 컨텍스트 낭비와 불필요한 정보 노출을 막을 수 있다. 🔌 AI 클라이언트에 연결하기 1. MCP Inspector로 먼저 테스트 바로 AI에 붙이기 전에, 공식 디버깅 도구인 MCP Inspector 로 도구 목록과 호출 결과를 확인할 수 있다. Postman 같은 역할이다. npx @modelcontextprotocol/inspector 브라우저가 열리면 Transport를 Streamable HTTP , URL을 http://localhost:8080/mcp 로 입력하고 연결한다. Tools 탭에서 search-incidents , recent-incidents 두 개가 보이면 성공이다. 파라미터를 직접 넣고 호출해서 응답 JSON도 확인할 수 있다. 2. Claude Code에 연결 claude mcp add --transport http incident http://localhost:8080/mcp 연결 후 이렇게 물어보면, > 최근 7일 동안 결제 서비스에 장애 있었어? 원인도 정리해줘 AI가 스스로 search-incidents 도구를 service="결제", days=7 로 호출하고, 조회 결과를 바탕으로 장애 건수와 원인을 요약해서 답변해 준다. 내가 한 일은 도구를 만들고 설명을 잘 써둔 것 뿐인데, 어떤 도구를 언제 쓸지는 LLM이 알아서 판단한다는 점이 신기했다. Cursor, Claude Desktop 등 MCP를 지원하는 다른 클라이언트에서도 같은 URL로 연결할 수 있다. 서버는 하나, 클라이언트는 무엇이든 — 이게 MCP의 핵심 장점이다. ⚠️ 주의할 점 항목 내용 인증 HTTP 기반 MCP 서버는 기본적으로 인증 없는 엔드포인트 가 열린다. localhost 밖으로 노출하려면 Spring Security 등으로 반드시 인증을 붙여야 한다. 권한 최소화 조회용 도구는 readOnlyHint = true 로 명시하고, 삭제·수정 같은 도구는 신중하게 만든다. LLM이 잘못 판단해서 호출할 수도 있기 때문이다. Description 품질 AI가 도구를 잘 못 쓴다면 코드보다 description 부터 의심하자. 응답 크기 조회 결과가 너무 크면 컨텍스트 윈도우를 낭비한다. 페이징이나 건수 제한을 두자. 민감 정보 반환한 데이터는 외부 LLM으로 전달된다. 개인정보·보안 정보는 DTO에서 제외한다. 🌸 마치며 💭 느낀 점 사실 AI 쪽은 제대로 공부해 본 적이 없어서, 처음에는 LLM, 토큰, MCP 같은 용어부터 낯설었다. "AI를 붙인다"고 하면 모델을 직접 학습시키거나 복잡한 파이썬 코드를 짜야 하는 줄 알았는데, 막상 해보니 내가 평소에 하던 일과 크게 다르지 않았다. @McpTool 은 @GetMapping 처럼 느껴졌고 description 은 Swagger 설명을 쓰는 느낌이었고 결국 "사람 대신 AI가 호출하는 API" 를 만든 것이었다 가장 신기했던 건, 나는 메서드를 만들고 설명만 적어뒀을 뿐인데 어떤 도구를 언제, 어떤 값으로 호출할지는 AI가 알아서 판단 한다는 점이었다. 반대로 말하면 설명을 대충 쓰면 AI도 대충 판단한다는 뜻이라, 코드만큼 설명을 잘 쓰는 것 이 중요하다는 걸 느꼈다. 또 AI가 내 DB 데이터를 직접 조회할 수 있다는 건 편하지만, 그만큼 인증이나 권한, 민감 정보 노출 은 더 신경 써야겠다는 생각이 들었다. 아직 깊이 있게 아는 건 아니지만, 백엔드 개발자도 AI를 "쓰는 사람"에서 한 발 나아가 "AI가 쓸 수 있는 기능을 만드는 사람" 이 될 수 있겠다는 걸 알게 된 계기였다. 📝 정리 처음엔 LLM이나 MCP가 거창한 기술처럼 느껴졌는데, 정리해 보니 LLM 은 "다음 단어를 예측하는 모델"이고, 학습한 것 밖의 정보는 모른다 그래서 Tool Calling 으로 외부 기능을 호출할 수 있게 했고 MCP 는 그 도구 연결 방식을 표준화한 프로토콜 이다 라는 흐름으로 이해할 수 있었다. 특히 Spring AI를 쓰면 기존 Service/Repository 코드는 그대로 두고 @McpTool 만 붙이면 되기 때문에, 이미 만들어 둔 Spring 프로젝트에도 부담 없이 적용해 볼 수 있을 것 같다. 다음 글에서는 반대로 Spring AI MCP Client 를 이용해서, 내 애플리케이션 안에서 외부 MCP 서버(GitHub, Slack 등)를 호출하는 에이전트를 만들어 볼 예정이다. 📚 참고 Model Context Protocol 공식 문서 Spring AI Reference - MCP Server Boot Starter Spring AI Reference - MCP Server Annotations Spring AI Reference - Upgrade Notes
velog
정보처리기사 를 준비하면서 공부할 내용에 대해 정리할 곳이 필요했는데, Notion과 git은 사용하고 있지만 개발 블로그는 따로 하고 있지 않아서 이번 기회에 시작하려고 함 ! 나는? 비전공자 출신 RPA 개발자임, 경력은 이제 곧 3년이 채워질 예정임, 중견기업 이상을 가기 위해 정처기는 큰 무기가 될 것 같다고 판단했음, 현재 필기는 2025년 8월, 전날 하루 벼락치기로 합격한 후 실기는 미뤄놨었음(그러지 말지 ᅲ) 2026년 3회차부터 공부를 시작하기로 마음 먹음!
Score: 54.39Confidence: 49%
View offervelog
RED 지표(요청·오류·지연시간) 및 서비스 간 호출 관계와 의존성을 보여주는 서비스 맵을 만들기 위한 데이터가 필요했고. . . → 이를 수집하기 위해 추가 적용하였던 OTel 컴포넌트들에 대해 다룬다. OTel Connector https://opentelemetry.io/docs/collector/components/connector/ OTel Collector 안에서 한 신호를 다른 신호로 바꿔 이어주는 컴포넌트 예를 들면... 입력: Trace 신호 (span들) 출력: Metric 신호 (calls 카운터, duration 히스토그램) 용도: Trace → RED Metric 생성 현재 환경에서는 span_metrics 와 service_graph 두 개 를 사용할 예정이며, 둘 다 Trace의 span을 읽어서 애플리케이션 Metric을 만든다. 사용한 Connector 모두 OTel Gateway 에서 동작한다. → Why? 전체 span을 한곳에 모아서 집계하여 RED 지표 같은 Metric을 생성하기 때문임. span_metrics https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/connector/spanmetricsconnector/README.md RED 지표: 요청 수·오류·지연시간 계산 service_graph https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/connector/servicegraphconnector/README.md Service Graph Metric: 서비스 A → B 호출 관계 계산 1. span_metrics 역할: Span ↓ 요청 수 오류 수 지연시간 histogram 예: apm-test-api 요청 100건 오류 3건 p95 420ms 를 만들기 위한 Metric을 생성함. 저장 위치: counter → otel_metrics_sum duration → otel_metrics_histogram 쉽게 애플리케이션의 RED 지표 시각화 용도인 데이터 생성기 라고 보면 됨. 2. service_graph 역할: apm-test-api CLIENT span + apm-test-downstream SERVER span ↓ apm-test-api → apm-test-downstream 처럼 서비스 간 관계를 찾음. AND.. 호출 수 실패 수 client/server latency unpaired span 같은 Metric을 생성함. 저장 위치: counter → otel_metrics_sum latency → otel_metrics_histogram 서비스맵을 만들기 위한 데이터 생성기 라고 보면 됨. 설정 위치 위에서 언급한 것처럼 둘 다 Runtime Agent가 아니라 otel-gateway Collector 설정에 추가함. Java Runtime Agent ↓ OTLP Trace ↓ otel-gateway ↓ 전체 Span ├─ span_metrics ├─ service_graph └─ tail_sampling 둘 다 Tail Sampling 전의 100% Span을 봐야함. 대략적인 정리 Java Runtime Agent → Span을 만든다 OTel Gateway Collector → Span을 받아 처리한다 span_metrics → Span → RED Metric service_graph → Span → Service Graph Metric tail_sampling → 어떤 Trace 원본을 저장할지 결정 Agent는 Span 생성, Connector는 Span 분석해서 Metric 생성, Tail Sampling은 Trace 저장 여부 결정.
Score: 54.39Confidence: 49%
View offervelog
포스트를 보면 영어 포스트임에도 불구하고 하단의 show_recent_posts 부분에는 언어 구분없이 모든 포스트가 노출되는걸 볼 수 있다. 이 부분을 수정해보자. 0.0.2 · sanghunka/ghost-multilingual-theme@0e2b7a3 Multilingual show_recent_posts_footer post.hbs 수정 {{#get "posts" filter="id:-{{id}}" limit="3" as |more_posts|}} {{#if more_posts}} <aside class="read-more-wrap outer"> <div class="read-more inner"> {{#foreach more_posts}} {{> "post-card"}} {{/foreach}} </div> </aside> {{/if}} {{/get}} 위 코드를 삭제하고 아래 코드를 추가해주자. {{#has tag="#en"}} {{#get "posts" filter="tag:en+id:-{{id}}" limit="3" as |more_posts|}} {{#if more_posts}} <aside class="read-more-wrap outer"> <div class="read-more inner"> {{#foreach more_posts}} {{> "post-card"}} {{/foreach}} </div> </aside> {{/if}} {{/get}} {{/has}} {{#has tag="#ko"}} {{#get "posts" filter="tag:ko+id:-{{id}}" limit="3" as |more_posts|}} {{#if more_posts}} <aside class="read-more-wrap outer"> <div class="read-more inner"> {{#foreach more_posts}} {{> "post-card"}} {{/foreach}} </div> </aside> {{/if}} {{/get}} {{/has}} 가장 최근 포스트 3개를 가져오는 부분을 수정했다. 포스트가 #en태그를 가지고 있을 경우, filter="tag:en"을 적용해주고 #ko태그를 가지고 있는 경우 filter="tag:ko"태그를 적용해준다. 결과 포스트의 언어태그와 일치하는 포스트만 하단에 노출된다. 다음글 고스트 CMS 다국어 블로그: 3. Language selector 추가 다국어 블로그 기초 설정을 완료했다. /와 /ko/ 경로를 사용하여 각 언어별 포스트를 구분하였다. 현재는 URL을 직접 입력해야만 영어 페이지에서 한국어 페이지로, 또는 그 반대로 이동할 수 있다. 사용자의 편의를 위해 언어를 선택할 수 있는 UI를 추가해보자. 0.0.3 · sanghunka/ghost-multilingual-theme@f5a2d86Add language selectorGitHubsanghunka default.hbs * <div class=“gh-head-actions”> 하단에 language-selector div 추가.</div> 원문: Sanghun Kang의 블로그
Score: 54.39Confidence: 49%
View offervelog
(출처: arXiv:2609.26634, Figure 1) Knowledge Pull Requests for Continual Document Authoring 저자 : Alexander Martin, Benjamin Van Durme (Johns Hopkins University) 공개일 : 2026년 9월 22일 (arXiv, cs.CL) arXiv : https://arxiv.org/abs/2609.26634 코드 : https://github.com/alexmartin1722/kpr 분류 : LLM · 문서 자동 저술(Continual Document Authoring) · 지식 통합 · 멀티링구얼 · RAG 🔖 TL;DR (한눈에) 기존 문서를 새 자료로 갱신하는 작업을 코드의 Pull Request처럼 다루는 프레임워크 KPR(Knowledge Pull Request) 을 제안한다. 소스 문서를 원자적 claim으로 분해 → 이미 있는 내용/충돌/무관 여부로 걸러내고 → 들어갈 섹션으로 라우팅 → 해당 섹션만 재작성한다. 산출물은 ChangeLog : "무슨 지식이 바뀌는가(Claim Proposal)"와 "문장이 어떻게 바뀌는가(Document Diff)"를 분리해 사람이 리뷰할 수 있게 만든다. 영어 위키백과를 49개 언어판으로 보강하는 실험에서 추가 정보 재현율(InfoR-A) 0.716 → 0.891 , 기존 내용 보존율 97.3%, 리뷰 클릭 수는 경쟁 기법의 절반 미만(11.2 vs 25.3). 다국어 QA에서 KPR로 갱신된 문서를 근거로 주면 평균 정확도 29.5 → 69.2 . 가장 어려운 100문항에서는 7B 모델(49점)이 웹 검색을 쓴 프런티어 모델(41점)을 이겼다. 한 줄 요약 : 문서를 매번 새로 쓰거나 통째로 덮어쓰는 대신, "어떤 사실이 왜 추가·보류되는지"를 리뷰 가능한 diff로 남기면서 점진적으로 갱신하는 방법. 📄 초록(Abstract) 완역 우리는 각각의 변경을 해석 가능하게 만드는 지속적 문서 저술(continual document authoring) 프레임워크인 Knowledge Pull Requests(KPRs) 를 소개한다. 문서는 다른 출처, 다른 언어, 다른 시점에서 새로운 지식이 드러날 때마다 지속적인 개정을 요구하지만, 기존 접근법들은 어떤 지식이 바뀌었는지에 대한 설명 없이 편집하거나 처음부터 다시 생성한다. KPR은 claim을 추출하고, 이를 필터링해 섹션으로 라우팅하며, 기존 내용과의 충돌을 표시함으로써 새로운 지식을 문서에 통합한다. 이 과정에서 어떤 지식이 변하는지(claim proposal)와 텍스트가 어떻게 변하는지(document diff)를 분리한 ChangeLog 를 생성한다. 우리는 여러 언어에 걸쳐 위키백과를 개정하는 과제와 RAGTIME에서 질의 기반 보고서를 갱신하는 과제로 KPR을 평가한다. KPR은 출처로부터 다시 쓰거나 처음부터 재생성하는 방식보다 더 많은 정보를 통합하고 기존 내용을 더 잘 보존하며, 동시에 생성된 토큰당 가장 많은 정보를 추가한다. 또한 KPR로 개정된 문서는, 다른 언어에만 기록된 지식을 찾아내지 못하는 검색 기능을 갖춘 프런티어 모델보다 질의응답을 더 잘 뒷받침한다. 요약하면 , 이 논문의 문제의식은 "문서 갱신을 LLM에게 맡기면 결과물만 나오고 근거가 남지 않는다"는 것이다. KPR은 갱신 단위를 문장이나 문단이 아니라 claim(원자적·탈문맥화된 사실 진술) 으로 내리고, 그 claim들이 통과·보류·기각된 기록을 문서와 함께 남긴다. 실험은 두 축으로 진행된다. 하나는 "이미 잘 정제된 다른 언어 문서에서 영어 문서로 지식을 옮길 수 있는가"(위키백과), 다른 하나는 "시간이 지나 새 문서가 들어왔을 때 보고서를 갱신할 수 있는가"(RAGTIME)다. 🧩 왜 이 문제가 중요한가 LLM으로 문서를 만드는 일은 이제 어렵지 않다. 정작 어려운 것은 이미 존재하는 문서를 계속 살려두는 일 이다. 사내 위키, 제품 문서, 리서치 리포트, 운영 런북은 모두 "한 번 쓰고 끝"이 아니라 새 자료가 생길 때마다 갱신되어야 한다. 그런데 현재 방식은 대체로 두 갈래로 갈린다. 첫째, 처음부터 다시 생성하기 . 논문이 지적하듯 오늘날의 deep research·보고서 생성 시스템 대부분이 이 방식이고, 이전 문서에 들어간 작업은 그냥 버려진다. 사람이 다듬어 놓은 표현, 조심스럽게 조율한 뉘앙스, 지난번 리뷰에서 고친 오류가 함께 사라진다. 둘째, 그냥 편집하도록 맡기기 . 결과 텍스트는 얻지만 무엇이 왜 바뀌었는지 알 수 없다. 텍스트 diff만으로는 "이 문장이 왜 고쳐졌는가"를 판단할 수 없기 때문에, 신뢰가 필요한 문서일수록 사람이 전부 다시 읽어야 한다. 저자들이 든 비유가 정확하다. 소프트웨어에서 우리는 코드를 남이 통째로 덮어쓰게 두지 않는다. PR을 열고, diff를 보고, 리뷰하고, 머지한다. 문서 지식에도 같은 장치가 필요하다는 것이 이 논문의 출발점이다. 특히 지식 충돌 — 한 자료는 승차 2.7%라 쓰고 다른 자료는 3%라 쓰는 상황 — 에서 모델이 조용히 한쪽을 골라버리는 대신 "여기 충돌이 있습니다"라고 올려주는 것 이 더 안전하다는 관점이다. 또 하나 흥미로운 문제 설정은 언어 장벽 이다. 어떤 사실이 인터넷에 공개돼 있고 검색 엔진에 색인돼 있어도, 그것이 영어가 아닌 위키백과 판에만 적혀 있으면 실질적으로 찾아지지 않는다. 논문은 이 구멍을 QA 실험으로 정량화한다. 🔬 방법론 1) KPR의 구성 요소 KPR은 두 개의 입력을 받는다. main (이미 작성된 대상 문서)과 sources (새 지식을 담고 있을 수 있는 문서들)이다. 목표는 main을 덮어쓰는 것이 아니라 "기존 내용과 함께 작업하며" 소스의 정보를 통합하는 것이다. 핵심 동작 단위는 claim , 즉 원자적이고 탈문맥화된 사실 진술이다. 문단 단위가 아니라 claim 단위로 내려가는 이유는 세 가지를 정밀하게 추적하기 위해서다. (a) 어떤 지식이 제안되고 있는지, (b) main의 어디에 들어가야 하는지, (c) 기존 내용과 충돌하는지. 그리고 산출물인 ChangeLog 가 KPR을 단순 재작성과 구분한다. ChangeLog는 두 부분으로 나뉜다. Claim Proposal : 새 claim들이 main의 어느 섹션(필요하면 새 섹션)에 들어갈지에 대한 매핑. 이미 main이 담고 있는 claim(coverage)과 문서의 저술 기준에 맞지 않는 claim(relevance)은 여기서 탈락한다. 그리고 기존 claim과 모순되는 claim은 조용히 해결하지 않고 리뷰용으로 표시(flag) 된다. Document Diff : 통합이 적용되면 문서가 어떻게 읽히게 되는지에 대한 텍스트 변경 기록. 사람 리뷰어는 이 위에서 충돌을 판정하고, claim을 승인/기각하고, 필터링 결정을 되돌릴 수 있다. 저자들은 이를 명시적으로 "소프트웨어 공학의 코드 리뷰와 유사하다"고 표현한다. 2) 3단계 파이프라인 논문은 KPR을 만들어내는 3단계 베이스라인 구현을 제시한다. Stage 1 — Claim Decomposition. LLM이 소스를 원자적·탈문맥화된 claim 집합으로 분해한다. main도 같은 방식으로 분해하지만, 이쪽은 온라인이 아니라 오프라인에서 미리 인덱스로 캐싱 해 둔다. 비영어 소스는 번역한 뒤 분해하지 않고 곧바로 영어 claim으로 분해(cross-lingual decomposition) 하는데, 이것이 더 충실한 claim을 만든다는 점을 부록 실험으로 보였다. (출처: arXiv:2609.26634, Figure 2) Stage 2 — Claim Proposal. 후보 claim마다 네 가지를 판단한다. coverage — main이 이미 담고 있는가 conflict — main의 기존 claim과 모순되는가 relevance — 문서의 저술 기준(질의 관련성 또는 저술 가이드라인)에 맞는가 routing — 어느 섹션에 속하는가 구현상 coverage와 conflict는 한 번의 분류 패스 로 함께 처리되어 각 소스 claim이 'covered' / 'conflicting' / 'absent' 중 하나로 라벨링된다. 이어서 relevance는 'absent' claim에만 적용되는데, RAGTIME 설정에서는 정보 요청(query) 기준으로 필터링하고, 위키백과 설정에서는 후보 자체가 같은 가이드라인으로 쓰인 문서에서 오기 때문에 필터를 걸지 않는다. 마지막으로 라우팅이 살아남은 claim을 기존 섹션에 배치하거나 새 섹션을 제안한다. 'covered'와 무관한 claim은 드롭 되고, 'conflicting' claim은 flag 된다. (출처: arXiv:2609.26634, Figure 3) Stage 3 — Document Diff. 승인된 proposal이 섹션 단위로 적용된다. claim을 하나 이상 받은 섹션(신규 또는 기존)만 재작성되고, 제안된 claim이 없는 섹션은 손대지 않는다. 결과를 원본 main과 diff하면 document diff가 나온다. 이 "건드리지 않는다"가 뒤에 나오는 보존율·클릭 수 지표의 근원이다. (출처: arXiv:2609.26634, Figure 4) 3) 비교 대상(베이스라인) 세 베이스라인 모두 claim proposal이 없다는 점이 공통이다. 이름 무엇을 조건으로 재작성하는가 KPR과의 차이 ConText 소스의 원문 텍스트 claim 분해 자체가 없음. WiNELL(Gangi Reddy et al., 2026)을 저자들이 각색한 것으로, 검색 단계 대신 LLM이 섹션 관련성을 분류 ConClaim 소스의 claim claim 분해와 라우팅은 KPR과 동일하지만 coverage·conflict·relevance 필터가 전혀 없다. 즉 "claim 리뷰가 없는 KPR" Scratch 모든 소스를 한 번에 기존 문서를 버리고 재생성. RAGTIME 설정에서만 사용 이 구성 덕분에 ConText → ConClaim → KPR이 깔끔한 단계적 ablation이 된다. 앞의 화살표는 "원문 대신 claim을 쓰는 효과", 뒤의 화살표는 "claim proposal(리뷰)을 추가하는 효과"를 각각 분리해 보여준다. 4) 구현 세부 분류·라우팅·재작성 모든 LLM 호출에 Qwen3.5-27B 하나만 사용한다(vLLM 서빙). 위키백과 문서는 섹션 구조가 이미 있어 그대로 쓰고, RAGTIME 보고서는 섹션이 없어 1라운드 보고서에 개요를 부여 해 모든 기법이 섹션 단위로 작동할 수 있게 했다. 실험에는 사람 리뷰어가 없다. 충돌로 표시된 claim은 해결되지 않고 재작성에서 보류(withheld) 된다. 저자들의 설명은 "충돌 해결은 어느 출처를 믿을지 결정하는 일이고, 출처 신뢰 판정은 여전히 열린 문제"라는 것이다. 이 점은 결과 해석에서 중요하다. 평가 지표 MiRAGE의 근거 판정기(support judge)도 Qwen3.5-27B이며, 비교 대상 프런티어 모델은 GPT-5.6("Sol") 로 reasoning effort를 최대로 설정했다. 5) 평가 지표 직관 품질은 MiRAGE 기반 두 축으로 본다. InfoP(Information Precision) : 출력의 서브클레임 중 소스가 뒷받침하는 비율. FActScore의 소스 제한 변형. "쓴 내용이 근거가 있는가." InfoR(Information Recall) : 반대 방향. 두 갈래로 쓴다. InfoR-R(retain) 은 원본 문서의 claim이 얼마나 보존됐는지, InfoR-A(add) 는 소스의 claim이 얼마나 반영됐는지. 편집 비용은 원본과 재작성본의 단어 단위 diff에서 뽑는다. WER (단어 편집률, 원본보다 많이 추가하면 100%를 넘을 수 있다), Click (리뷰어가 승인 버튼을 눌러야 하는 연속 편집 블록 수 — 길이와 무관하게 블록 하나당 1클릭이므로, 잘게 흩어진 수정이 큰 덩어리 하나보다 비싸다), Tok (추가된 토큰 수), Presv (원본이 그대로 보존된 비율), Add (원본 대비 길이 배율). 저자들은 WER·Click·Tok을 비용으로, Presv·Add를 "변경의 모양"으로 구분하며, 후자는 높거나 낮은 쪽이 일방적으로 좋은 것은 아니라고 명시한다. 📊 실험 결과 실험 1: 위키백과 교차언어 개정 영어 위키백과를 main, 다른 언어판을 sources로 둔다. MegaWika 2.0에서 599개 문서 를 교차언어 링크 수 분포에 맞춰 샘플링했고, 영어를 제외한 49개 언어 가 소스로 등장한다. 문서 품질 (Table 2) Method InfoP InfoR-R InfoR-A ConText 0.837 0.946 0.716 ConClaim 0.853 0.943 0.786 KPR 0.878 0.950 0.891 두 단계 개선이 뚜렷하다. 원문 대신 claim을 조건으로 주는 것만으로 정밀도 0.837 → 0.853, 추가 정보 재현율 0.716 → 0.786이 오르고, 여기에 claim proposal을 붙이면 0.878 / 0.891로 한 번 더 오른다. 이득이 더 큰 쪽은 추가 정보 재현율 이다. 기존 내용 보존(InfoR-R)은 세 기법 모두 0.943~0.950으로 높아, 실질적 차이는 "소스 지식을 얼마나 실제로 통합했는가"에서 갈린다. 다국어 QA (Table 1, Multi-QA) — 각 조건의 문서를 근거로 주고 답을 맞히게 한 정확도다. Model CB(문서 없음) EW(원본 영어 문서) ConText ConClaim KPR Qwen3.5-27B 36.3 15.5 41.7 54.6 67.8 Qwen3-30B 31.0 29.4 49.1 59.9 70.7 Gemma-4-31B 35.9 14.6 39.5 52.7 66.3 Llama-3.3-70B 43.5 31.9 52.9 62.8 74.0 Llama-4-Scout 33.9 34.5 42.3 48.9 54.8 Mixtral-8x7B 36.4 39.0 55.7 64.2 74.5 Nemotron-3-120B 41.6 41.3 57.1 66.1 76.6 평균 36.9 29.5 48.4 58.5 69.2 눈에 띄는 것은 원본 영어 문서(EW, 평균 29.5)가 문서를 아예 안 주는 것(CB, 36.9)보다도 낮다 는 점이다. 질문된 사실이 영어 문서에 없기 때문이고, 일부 모델은 추측 대신 성실하게 답을 거부한다(각주: Gemma-4와 Qwen3.5는 약 70%에서 abstain). 세 재작성 기법 모두 이를 회복하지만 KPR의 회복량이 가장 크다. 반대로 영어 QA(Table 1, En-QA) 에서는 원본 문서가 평균 92.0으로 가장 높고, 세 재작성본은 87.6 / 88.0 / 87.7로 서로 0.5% 안쪽이다. 즉 교차언어 지식을 통합하는 대가로 기존 내용이 유의미하게 희생되지는 않는다. 작은 모델도 같은 이득 (Table 3) Model CB EW KPR Qwen3.5-9B 37.4 15.5 44.8 Qwen3-8B 33.5 35.8 61.4 Llama-3.1-8B 38.9 33.8 61.5 OLMo-3-7B 25.5 25.1 51.1 가장 어려운 100문항 (Table 4) — 어떤 오픈 모델도 closed-book으로 맞히지 못한 다국어 질문 100개다. (WS = 웹 검색, -T = 질문을 소스 언어로 번역) Model CB CB-T EW KPR WS WS-T Qwen3.5-9B 2 3 2 49 – – Qwen3-8B 4 4 9 56 – – Llama-3.1-8B 2 1 7 56 – – OLMo-3-7B 2 2 13 50 – – Qwen3.5-27B 0 5 3 52 – – Qwen3-30B 0 3 12 54 – – Gemma-4-31B 0 4 4 54 – – Llama-3.3-70B 0 5 8 51 – – GPT-5.6 (Sol) 13 20 32 62 38 41 이 표가 논문에서 가장 인상적인 부분이다. 웹 검색을 붙인 GPT-5.6이 38점(번역 시 41점)인데, 원본 영어 문서만 준 경우의 32점보다 겨우 조금 높다. 반면 가장 작은 7 9B 모델이 KPR로 갱신된 문서를 근거로 받으면 49 56점 을 낸다. 저자들의 해석: 소스가 다른 언어판 위키백과이므로 그 지식은 공개돼 있고 색인도 되어 있지만, 검색으로 표면화되지 않는다. 편집 비용 (Table 5) Method WER Click Tok Presv Add KPR 131 11.2 1,299 97.3 2.3 ConClaim 92 25.3 898 94.6 1.8 ConText 138 14.7 1,374 96.9 2.3 KPR은 원본을 가장 많이 보존하면서(97.3%) 가장 적은 연속 편집(11.2 클릭)으로 변경을 만든다. KPR과 ConText는 추가 분량이 사실상 같은데(둘 다 Add 2.3배, Tok 1,299 vs 1,374) KPR이 훨씬 많은 소스 지식을 통합한다. ConClaim은 전체 변경량이 가장 작지만(WER 92, Add 1.8배) 승인 클릭은 KPR의 두 배가 넘는다(25.3) . claim proposal이 없으면 섹션에 라우팅된 모든 claim이 그대로 재작성에 넘어가, 바뀔 필요 없던 내용까지 모델이 건드리기 때문이다. 실험 2: RAGTIME 질의 기반 보고서 갱신 RAGTIME은 페르소나와 질의를 받아 다국어 문서 집합에 대해 RAG를 수행하는 다국어 보고서 생성 과제다(여기서는 검색 대신 관련성 판정을 직접 사용). 저자들은 2라운드 변형 세 가지를 만들었다. Temporal (발행일 기준 전/후 절반), Conflict (OR 너깃의 상충 답변이나 AND 너깃의 상보 조각을 서로 다른 라운드에 배치), Balanced (라운드별 너깃 수를 최대한 같게). 설정 Round-2 InfoP InfoR-R InfoR-A Temporal R1 Main 0.928 0.416 – Temporal Scratch 0.882 0.475 0.223 Temporal ConText 0.620 0.649 0.632 Temporal KPR 0.729 0.709 0.682 Conflict R1 Main 0.904 0.430 – Conflict Scratch 0.875 0.456 0.286 Conflict ConText 0.666 0.625 0.429 Conflict KPR 0.811 0.782 0.571 Balanced R1 Main 0.961 0.453 – Balanced Scratch 0.862 0.558 0.326 Balanced ConText 0.692 0.739 0.537 Balanced KPR 0.841 0.856 0.632 KPR이 세 설정 모두에서 InfoR-R과 InfoR-A 최고이고, 재작성 기법들 중 정밀도도 가장 높다. Scratch는 라운드 간 정보를 가장 적게 추가한다 (InfoR-A 0.223 / 0.286 / 0.326). 처음부터 다시 쓰는 방식이 실제로 얼마나 손해인지 보여주는 숫자다. Conflict 설정이 특히 흥미롭다. ConText는 여기서 InfoR-A가 0.429로 떨어지는데(Temporal 0.632, Balanced 0.537), 충돌을 표시할 장치가 없으니 재작성 중에 암묵적으로 해결해야 하고 그 과정이 정보 통합 자체를 억제하는 것으로 보인다. KPR은 충돌 claim을 보류하면서 다른 설정과 비슷한 비율로 새 사실을 추가하고, 의미 있는 양을 통합하는 기법 중 가장 높은 정밀도(0.811 vs ConText 0.666)를 유지한다. 또 하나: 모든 기법이 1라운드 보고서 자신보다 1라운드 너깃을 더 많이 재현한다 (Temporal에서 R1 Main 0.416 < Scratch 0.475 < ConText 0.649 < KPR 0.709). 1라운드 정보 일부가 2라운드 문서에도 다시 등장하므로, 2라운드 소스를 통합하는 과정에서 원래 보고서가 놓쳤던 너깃이 회수된다. 점진적 기법들이 1라운드 문서를 다시 읽지 않았는데도 그렇다. 편집 비용 (Table 7) Method WER Click Tok Presv Add Scratch 130 56.1 770 37.3 1.2 ConText 330 12.5 3,091 98.2 4.3 KPR 372 20.5 3,475 96.3 4.7 Scratch의 높은 정밀도는 "짧지만 확신 있는 보고서"라는 모양과 맞아떨어지지만(Presv 37.3, Add 1.2배), 리뷰 비용은 가장 비싸다(56.1 클릭). 재생성이라 우연한 n-gram 중복만 보존되기 때문이다. 부록의
Score: 54.4Confidence: 49%
Score: 54.4Confidence: 49%
Score: 54.39Confidence: 49%