웹을 자율로 탐색·조사하는 에이전트 모노레포입니다. @awr/reader(웹페이지를 LLM이 읽기 좋게 변환하는 도구)와 그 위에서 동작하는 @awr/research(심층 검색 에이전트), 두 패키지로 구성됩니다.
아래는 변환기 @awr/reader에 대한 설명입니다. 웹 페이지를 LLM이 탐색하기 좋은 형식으로 변환합니다.
HTML을 그대로 LLM에 주입하는 대신, 두 계층으로 분리합니다.
page.md: 본문·메뉴·표·입력창·버튼을 마크다운으로 정리하고, 링크는[L1]·[L2]같은 ID로만 표시links.json/elements.json: 링크 ID와 실제 URL, 요소 ID와 상호작용 정보를 별도 저장
LLM은 page.md만 읽고 어떤 링크를 따라갈지 판단한 뒤, 필요한 URL만 꺼내 쓸 수 있습니다.
npm install
npx playwright install chromium
npm run buildGoogle 검색처럼 봇 탐지가 있는 사이트를 사용하려면 시스템에 Chrome이 설치되어 있어야 합니다 (
--stealth옵션).
node packages/reader/dist/cli.js convert "https://example.com" --out ./out/exampleout/example/ 에 page.md, page.json, links.json, elements.json 생성.
옵션:
| 옵션 | 기본값 | 설명 |
|---|---|---|
--out <dir> |
out |
출력 디렉토리 |
--page-id <id> |
P1 |
페이지 ID |
--no-scroll |
— | 자동 스크롤 비활성화 |
--max-scrolls <n> |
15 |
최대 스크롤 횟수 |
--scroll-wait-ms <n> |
800 |
스크롤 후 대기 시간(ms) |
--stable-rounds <n> |
2 |
안정화 판단 반복 횟수 |
--stealth |
— | bot 탐지 우회 (Chrome 필요) |
node packages/reader/dist/cli.js resolve P1 L23 --state ./out/example
# https://example.com/some/pathnode packages/reader/dist/cli.js open P1 L23 --state ./out/example --out ./out/next --page-id P2LLM이 자율적으로 웹을 탐색하여 질문에 답변합니다. 하나의 재귀 Researcher가 search/paginate/read_sections/delegate/delegate_parallel/done 행동을 상태별 스키마로 제한하며 수행하고, 루트는 직접 검색하지 않고 먼저 하위 리서처에게 발견 작업을 위임합니다.
node packages/research/dist/cli.js --query "질문"옵션:
| 옵션 | 기본값 | 설명 |
|---|---|---|
--query <str> |
필수 | 검색할 질문 |
--model <str> |
gpt-5.4-mini |
OpenAI 모델명 |
--config <path> |
llm-search.config.json |
탐색 예산 설정 파일 |
--debug |
— | 디버그 로그 저장 |
--log-dir <dir> |
. |
로그 저장 경로 |
--env <path> |
.env |
환경변수 파일 경로 |
llm-search.config.json의 limits에서 maxRounds, maxSearches, maxExplores, maxParallel, maxDepth, maxChildCallsPerAgent 한도를 조정할 수 있습니다. 설정 파일이 없으면 코드 기본값을 사용합니다.
OpenAI API 키가 필요합니다. 프로젝트 루트에 .env 파일을 생성하세요.
OPENAI_API_KEY=sk-...
--debug 활성화 시 에이전트 실행 과정 전체를 JSONL 파일로 저장합니다.
node packages/research/dist/cli.js --query "질문" --debug --log-dir ./logs터미널에는 실시간 상태가 출력됩니다. logs/researcher-*.jsonl에 모든 LLM 호출 내역과 토큰 사용량이 시간순으로 기록됩니다. 각 줄은 유효한 JSON 한 개이며, 에이전트 깊이만큼 좌측 공백으로 들여써서 호출 계층을 시각화합니다.
import { convertPage, resolveLink, openLink, convertHtml } from "@awr/reader";
// 페이지 변환
const result = await convertPage("https://example.com", {
pageId: "P1",
maxScrolls: 15,
stealth: false,
});
// result.markdown — LLM에 전달할 마크다운
// result.links — 링크 레지스트리
// result.elements — 요소 레지스트리
// result.page — 구조화된 AST
// 링크 해석
const link = await resolveLink("./out/example", "P1", "L23");
// link.url — 실제 URL
// 링크 열고 다음 페이지 변환
const next = await openLink("./out/example", "P1", "L23", { pageId: "P2" });
// HTML 직접 변환 (브라우저 없이)
const result = convertHtml(htmlString, "https://source-url.com", { pageId: "P1" });# NAVER
- Page ID: P1
- Host: www.naver.com
- Links: 108
- Elements: 2
## Navigation
- 메일 [L1]
- 카페 [L2]
[input#I1: 검색어를 입력해 주세요.]
[button#B1: 검색]
## Main Content
...{
"pageId": "P1",
"sourceUrl": "https://www.naver.com/",
"links": {
"L1": {
"id": "L1",
"text": "메일",
"url": "https://mail.naver.com/",
"kind": "external",
"sourcePath": "div#header > a"
}
}
}{
"pageId": "P1",
"sourceUrl": "https://www.naver.com/",
"elements": {
"I1": { "id": "I1", "type": "input", "text": "검색어를 입력해 주세요.", "inputType": "text" },
"B1": { "id": "B1", "type": "button", "text": "검색" },
"S1": { "id": "S1", "type": "select", "options": ["10개 보기", "20개 보기"] }
}
}- 복잡한 캘린더, 캐러셀, 탭 UI, 무한 스크롤은 완벽히 재현되지 않을 수 있습니다.
- 이미지 자체의 시각 정보는 추출하지 않습니다 (alt/title/캡션만 활용).
- 로그인 상태나 지역·개인화 설정에 따라 출력이 달라질 수 있습니다.
- 광고 제거는 보수적으로 동작하므로 일부 광고성 콘텐츠가 남을 수 있습니다.
npm run build # TypeScript 컴파일
npm test # Vitest 테스트 실행