"시어엔진에 이런 데이터 전용 API가 있나" → 253개 엔진 중 하드웨어 스펙 엔진은 0개였다(geizhals는 403 봇차단, ddg definitions는 GPU 모델명에 빈 응답). 대신 GitHub에서 RightNow-AI/RightNow-GPU-Database(Apache 2.0, TechPowerUp 기반, 2,824개 GPU, 55필드)를 찾았고, README엔 언급이 없지만 실물엔 RTX 50 시리즈가 들어있는 것을 데이터를 받아 확인했다. 이게 지금까지의 우회로보다 나은 이유: - nvidia.com 공식 페이지는 스펙 키워드 0개(JS 렌더링), techpowerup은 봇 차단. HTML 스크래핑으로 살아있는 출처가 nanoreview 하나뿐이었다 - 봇차단·JS·파싱실패·죽은링크가 전부 없고, 캐시 워밍 후 64ms - 무엇보다 모든 레코드에 releaseDate가 있다. 5060은 2025-05-19다. 이 사태의 출발점이던 "5060은 미출시" 환각은 데이터와 만나는 순간 성립하지 않는다 — 가드는 쓰인 거짓말을 잡지만, 이건 쓸 이유를 없앤다 신선도가 진짜 리스크라 거기에 설계를 집중했다. 호스팅 API가 아니라 GitHub 저장소라서, 저쪽이 멈추면 우리도 멈추고 그러면 지금 고치는 실패가 데이터 계층에서 재현된다. 그래서 주간 갱신 + 결과에 캐시 나이를 항상 같이 실어 낡은 답이 조용히 틀리는 대신 눈에 띄게 낡도록 했다. 갱신은 백그라운드라 질문을 느리게 만들지 않고, 실패해도 옛 캐시로 계속 답한다. 도구가 아니라 주입으로 넣은 이유: 모델이 도구를 안 부른다. 로그 3,300줄 에서 web_fetch 호출 1번, GPU 질문엔 0번이었다(오늘 같은 결론 네 번째). - extractGpuMentions: 맥락이 있을 때만 맨숫자를 모델명으로 본다. "4080이 5060보다"는 잡고 "2024년 매출 3800억"은 안 잡는다 — 무관한 답변에 스펙이 끼어들면 그 자체가 오염이다 - findGpu: "RTX 4080"은 SUPER/Mobile 이름에도 부분일치하므로 변형에 가중치를 줘 기본 카드를 고른다. 데스크탑 질문에 노트북 칩 수치를 물려주면 조용히 틀린 답이 된다 - 캐시 쓰기는 write-then-rename (config.json 3회 손상과 같은 실패 방지) 실측: RTX 4080 FP32 48.74 TFLOPS / 716.8 GB/s vs RTX 5060 19.18 TFLOPS / 448 GB/s — 2.54배 차이를 근거를 갖고 말할 수 있게 됐다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107 lines
4.9 KiB
TypeScript
107 lines
4.9 KiB
TypeScript
/**
|
|
* gpu-specs — 모델명 인식과 카드 선택
|
|
*
|
|
* 순수 함수만 테스트한다. 다운로드/캐시는 네트워크와 디스크에 의존하므로 여기서 다루지 않는다.
|
|
*
|
|
* 이 테스트가 지키는 두 가지:
|
|
* 1) 맨숫자를 언제 모델명으로 볼 것인가 — "4080이 5060보다 빠른가"는 잡아야 하지만
|
|
* "2024년 실적"과 "1080p 영상"은 잡으면 안 된다. 무관한 답변에 GPU 스펙이 끼어들면
|
|
* 그 자체가 오염이다.
|
|
* 2) 변형 모델 선택 — "RTX 4080"은 SUPER/Mobile 이름에도 부분일치하는데, 데스크탑 질문에
|
|
* 노트북 칩 수치를 물려주면 조용히 틀린 답이 된다.
|
|
*/
|
|
|
|
import { test, describe } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { extractGpuMentions, findGpu, formatGpuRecord, type GpuRecord } from '../src/tools/gpu-specs';
|
|
|
|
const GPUS: GpuRecord[] = [
|
|
{ name: 'GeForce RTX 4080', vendor: 'nvidia', releaseDate: '2022-09-20', architecture: 'Ada Lovelace', processSize: 5, shaders: 9728, tensorCores: 304, baseClock: 2205, boostClock: 2505, memorySize: 16, memoryType: 'GDDR6X', memoryBus: 256, memoryBandwidth: 716.8, fp32: 48.74, tdp: 320, url: 'https://example.invalid/4080' },
|
|
{ name: 'GeForce RTX 4080 SUPER', vendor: 'nvidia', releaseDate: '2024-01-31' },
|
|
{ name: 'GeForce RTX 4080 Mobile', vendor: 'nvidia', releaseDate: '2023-02-22' },
|
|
{ name: 'GeForce RTX 5060', vendor: 'nvidia', releaseDate: '2025-05-19', memoryBandwidth: 448, fp32: 19.18 },
|
|
{ name: 'GeForce RTX 5060 Ti 16 GB', vendor: 'nvidia', releaseDate: '2025-04-16' },
|
|
{ name: 'GeForce RTX 5060 Mobile', vendor: 'nvidia', releaseDate: '2025-05-01' },
|
|
{ name: 'Radeon RX 7900 XTX', vendor: 'amd', releaseDate: '2022-12-13' },
|
|
{ name: 'Radeon RX 7900 XT', vendor: 'amd', releaseDate: '2022-12-13' },
|
|
];
|
|
|
|
describe('extractGpuMentions — 모델명 인식', () => {
|
|
test('시리즈 표기가 붙은 형태', () => {
|
|
const m = extractGpuMentions('RTX 4080 vs RTX 5060 performance comparison specs');
|
|
assert.deepEqual(m.map(x => `${x.series} ${x.number}`), ['rtx 4080', 'rtx 5060']);
|
|
});
|
|
|
|
test('접미사(Ti/SUPER/XTX)를 보존한다', () => {
|
|
assert.equal(extractGpuMentions('RTX 5060 Ti 리뷰')[0].suffix, 'ti');
|
|
assert.equal(extractGpuMentions('RX 7900 XTX 성능')[0].suffix, 'xtx');
|
|
});
|
|
|
|
test('GPU 맥락이 있으면 맨숫자도 모델명으로 본다', () => {
|
|
// 사용자가 실제로 친 형태. "RTX"가 한 번만 나오고 나머지는 숫자만 쓴다.
|
|
const m = extractGpuMentions('RTX 4080이 5060 보다 얼마나 빠르지?');
|
|
assert.deepEqual(m.map(x => x.number).sort(), ['4080', '5060']);
|
|
});
|
|
|
|
test('GPU 맥락이 없으면 맨숫자를 건드리지 않는다', () => {
|
|
assert.deepEqual(extractGpuMentions('2024년 매출이 3800억이었다'), []);
|
|
assert.deepEqual(extractGpuMentions('아파트 3800세대 분양'), []);
|
|
});
|
|
|
|
test('연도 꼴은 맥락이 있어도 모델명으로 보지 않는다', () => {
|
|
const m = extractGpuMentions('이 GPU는 2022년에 나왔다');
|
|
assert.deepEqual(m, []);
|
|
});
|
|
|
|
test('중복은 한 번만', () => {
|
|
const m = extractGpuMentions('RTX 4080 성능, RTX 4080 가격');
|
|
assert.equal(m.length, 1);
|
|
});
|
|
});
|
|
|
|
describe('findGpu — 변형 모델에 밀리지 않고 기본 카드를 고른다', () => {
|
|
const pick = (q: string) => findGpu(extractGpuMentions(q)[0], GPUS)?.name;
|
|
|
|
test('"RTX 4080"은 SUPER나 Mobile이 아니라 기본 카드', () => {
|
|
assert.equal(pick('RTX 4080 specs'), 'GeForce RTX 4080');
|
|
});
|
|
|
|
test('"RTX 5060"은 Ti나 Mobile이 아니라 기본 카드', () => {
|
|
assert.equal(pick('RTX 5060 specs'), 'GeForce RTX 5060');
|
|
});
|
|
|
|
test('접미사를 명시하면 그 변형을 고른다', () => {
|
|
assert.equal(pick('RTX 4080 SUPER 스펙'), 'GeForce RTX 4080 SUPER');
|
|
assert.equal(pick('RTX 5060 Ti 스펙'), 'GeForce RTX 5060 Ti 16 GB');
|
|
});
|
|
|
|
test('XT와 XTX를 구분한다', () => {
|
|
assert.equal(pick('RX 7900 XTX 성능'), 'Radeon RX 7900 XTX');
|
|
assert.equal(pick('RX 7900 XT 성능'), 'Radeon RX 7900 XT');
|
|
});
|
|
|
|
test('데이터에 없는 모델은 null', () => {
|
|
assert.equal(findGpu({ series: 'rtx', number: '9090', suffix: '', raw: 'RTX 9090' }, GPUS), null);
|
|
});
|
|
});
|
|
|
|
describe('formatGpuRecord — 판단에 필요한 필드만', () => {
|
|
const out = formatGpuRecord(GPUS[0]);
|
|
|
|
test('출시일이 들어간다 — "미출시" 환각을 원천 차단하는 필드다', () => {
|
|
assert.match(out, /2022-09-20/);
|
|
});
|
|
|
|
test('비교에 쓰이는 수치가 들어간다', () => {
|
|
assert.match(out, /716\.8 GB\/s/);
|
|
assert.match(out, /FP32 48\.74 TFLOPS/);
|
|
assert.match(out, /9728/);
|
|
});
|
|
|
|
test('없는 필드는 빈 줄로 남기지 않는다', () => {
|
|
const sparse = formatGpuRecord({ name: 'GeForce RTX 5060', releaseDate: '2025-05-19' });
|
|
assert.equal(sparse.split('\n').some(l => l.trim() === ''), false);
|
|
assert.match(sparse, /2025-05-19/);
|
|
});
|
|
});
|