/** * splitComparisonQuery — 비교 질문을 대상별 검색으로 쪼개기 * * 2026-08-10에 "A vs B로 합치지 말고 따로 검색하라"를 web_search 설명에 넣었는데, 08-11 프로덕션 * 로그에서 모델이 세 턴 연속으로 정확히 금지된 형태를 그대로 날렸다: * web_search({"query":"RTX 4080 vs RTX 5060 performance comparison specs"}) * 매번 technical.city 링크가 죽어 있었고(2/5 dead) 답변은 수치 0건의 일반론이었다. 설명으로 * 강제가 안 되니 코드에서 쪼갠다. * * 이 테스트가 지키는 선: 쪼갤 만한 것만 쪼갠다. "Lakers vs Celtics"를 "Lakers"/"Celtics" 두 * 검색으로 만들면 원래 질문의 의미가 사라진다 — 오탐 쪽이 더 비싸다. */ import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; import { splitComparisonQuery, hasSpecContent } from '../src/tools/web'; describe('쪼개야 하는 쿼리', () => { test('실제 사고 쿼리 — 비교 동사는 버리고 속성어는 남긴다', () => { const r = splitComparisonQuery('RTX 4080 vs RTX 5060 performance comparison specs'); assert.deepEqual(r, { a: 'RTX 4080 performance specs', b: 'RTX 5060 performance specs' }); }); test('속성어가 없어도 양쪽이 모델명 꼴이면 쪼갠다', () => { const r = splitComparisonQuery('RTX 4080 vs RTX 5060'); assert.deepEqual(r, { a: 'RTX 4080 specs', b: 'RTX 5060 specs' }); }); test('어제 검증한 대역폭 케이스', () => { const r = splitComparisonQuery('RTX 3090 vs RTX 4080 Super memory bandwidth'); assert.ok(r); assert.ok(r!.a.startsWith('RTX 3090')); assert.ok(r!.b.startsWith('RTX 4080 Super')); assert.ok(r!.a.includes('bandwidth') && r!.b.includes('bandwidth')); }); test('한국어 "와/과 ... 비교"도 쪼갠다', () => { assert.deepEqual( splitComparisonQuery('RTX 4080과 5060 성능 비교'), { a: 'RTX 4080 성능', b: '5060 성능' }, ); }); test('vs. / versus 표기도 인식한다', () => { assert.ok(splitComparisonQuery('M4 Max vs. M3 Ultra benchmark')); assert.ok(splitComparisonQuery('A100 versus H100 tflops')); }); }); describe('쪼개면 안 되는 쿼리', () => { test('스포츠 대진 — 속성어도 모델번호도 없다', () => { assert.equal(splitComparisonQuery('Lakers vs Celtics'), null); }); test('일반 검색은 건드리지 않는다', () => { assert.equal(splitComparisonQuery('오늘 서울 날씨'), null); assert.equal(splitComparisonQuery('RTX 4080 memory bandwidth'), null); }); test('비교 대상이 셋이면 판단하지 않는다', () => { assert.equal(splitComparisonQuery('A vs B vs C specs'), null); }); test('"결과"처럼 과/와로 끝나는 단어에 잘못 걸리지 않는다', () => { assert.equal(splitComparisonQuery('검색결과 스펙'), null); assert.equal(splitComparisonQuery('효과 성능'), null); }); test('빈 입력', () => { assert.equal(splitComparisonQuery(''), null); assert.equal(splitComparisonQuery(null as any), null); }); }); /** * hasSpecContent — 가져온 본문에 진짜 스펙이 실려 있는지 * * 이 판정이 필요한 이유는 2026-08-11 실측이다. 비교 검색이 물어오는 상위 세 출처를 실제로 * 가져와 봤더니, 제조사 공식 페이지가 셋 중 가장 쓸모없었다: * nvidia.com 6016자, 스펙 키워드 0개 (스펙 표가 JS 렌더링, 본문은 홍보 문구) * techpowerup 275자, 스펙 키워드 0개 (봇 차단) * nanoreview Cores/TMUs/Boost Clock/Bandwidth/TFLOPS 전부 있음 * 이 필터가 없으면 "Game Changer" 6KB가 모델 컨텍스트를 차지한다. */ describe('hasSpecContent — 본문에 수치가 실렸는지 판정', () => { test('NVIDIA 공식 페이지 본문은 통과시키지 않는다 (실측 발췌)', () => { assert.equal(hasSpecContent( 'GeForce RTX 5060 Family Graphics Cards | NVIDIA. This site requires Javascript in order to ' + 'view all its content. Game Changer. The NVIDIA GeForce RTX 5060 Ti and RTX 5060, powered by ' + 'the NVIDIA Blackwell architecture, enables game-changing AI capabilities.', ), false); }); test('봇 차단 페이지도 통과시키지 않는다 (실측 발췌)', () => { assert.equal(hasSpecContent( 'NVIDIA GeForce RTX 5060 Specs | TechPowerUp GPU Database. Automated bot check in progress. ' + 'Your browser must support Javascript.', ), false); }); test('nanoreview 본문은 통과시킨다 (실측 발췌)', () => { assert.equal(hasSpecContent( 'GeForce RTX 5060. Graphics Processor: GB206-250. Cores: 3840. TMUs / ROPs: 120 / 48.', ), true); }); test('대역폭·클럭 표기도 스펙으로 본다', () => { assert.equal(hasSpecContent('Memory Bandwidth: 936 GB/s'), true); assert.equal(hasSpecContent('Boost Clock: 2505 MHz'), true); assert.equal(hasSpecContent('CUDA Cores: 9728'), true); }); test('숫자 없는 일반 문장은 스펙이 아니다', () => { assert.equal(hasSpecContent('이 카드는 게이밍 성능이 매우 뛰어납니다.'), false); assert.equal(hasSpecContent(''), false); }); });