Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,13 @@ export class LlmClient {
}

private async chat(messages: ChatMessage[], options: ChatOptions = {}): Promise<string> {
const maxTokens = options.maxTokens ?? 8192
const body: Record<string, unknown> = {
model: this.model,
messages,
stream: false,
temperature: options.temperature ?? 0.2,
max_tokens: options.maxTokens ?? 8192,
max_tokens: maxTokens,
}

let response: Response
Expand Down Expand Up @@ -120,9 +121,23 @@ export class LlmClient {
this.totalUsage.total_tokens += data.usage.total_tokens ?? 0
}

const content = data.choices?.[0]?.message?.content ?? ''
const choice = data.choices?.[0]
const finishReason = choice?.finish_reason ?? 'unknown'
const usage = data.usage
log.info(
`모델 응답 — finish_reason=${finishReason}` +
(usage ? `, 토큰 ${usage.prompt_tokens ?? 0} in / ${usage.completion_tokens ?? 0} out` : ''),
)

// max_tokens에서 잘린 응답은 `<tool_call>` 이 닫히지 않아 파싱에서 통째로 버려진다.
// 그러면 결과만 봐서는 모델이 도구를 안 부른 것과 구분되지 않으므로 여기서 남긴다.
if (finishReason === 'length') {
log.warn(`응답이 max_tokens(${maxTokens})에서 잘렸다 — 도구 호출이 온전하지 않을 수 있다`)
}

const content = choice?.message?.content ?? ''
if (!content.trim()) {
throw new LlmError(`모델이 빈 응답을 반환했다 (finish_reason=${data.choices?.[0]?.finish_reason ?? 'unknown'})`)
throw new LlmError(`모델이 빈 응답을 반환했다 (finish_reason=${finishReason})`)
}
return content
}
Expand Down
35 changes: 33 additions & 2 deletions src/review/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ function retryNudge(): string {
function collectToolCalls(toolCalls: ToolCall[]): ReviewResult {
const summaries: string[] = []
const findings: RawFinding[] = []
let malformed = 0
let unknown = 0

for (const call of toolCalls) {
if (call.name === SUMMARY_TOOL) {
Expand All @@ -204,6 +206,7 @@ function collectToolCalls(toolCalls: ToolCall[]): ReviewResult {
}
if (call.name !== FINDING_TOOL) {
log.warn(`모델이 알 수 없는 도구를 호출했다: ${call.name}`)
unknown++
continue
}

Expand All @@ -212,13 +215,22 @@ function collectToolCalls(toolCalls: ToolCall[]): ReviewResult {
findings.push(parsed.data)
continue
}
malformed++
const issues = parsed.error.issues
.slice(0, 3)
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
.join('; ')
log.warn(`지적 하나가 스키마와 맞지 않아 버렸다 — ${issues}`)
}

// 지적이 0건일 때 원인을 가릴 수 있어야 한다 —
// 모델이 요약만 낸 것과, 낸 지적이 검증에서 떨어진 것은 서로 다른 문제다.
log.info(
`도구 호출 ${toolCalls.length}건 — 요약 ${summaries.length}, 지적 ${findings.length}` +
(malformed ? `, 형식 오류 ${malformed}` : '') +
(unknown ? `, 모르는 도구 ${unknown}` : ''),
)

return { summary: summaries.join('\n\n'), findings }
}

Expand Down Expand Up @@ -248,13 +260,19 @@ export function prepareFindings(
const severityOrder = { critical: 0, major: 1, minor: 2, nit: 3 }
const seen = new Set<string>()
const candidates: Finding[] = []
// 여기서 걸러진 지적은 인라인에도 요약에도 실리지 않는다 — 사라진 이유를 셈해 남긴다
const dropped = { severity: 0, file: 0, duplicate: 0 }

for (const raw of result.findings) {
if (!meetsSeverity(raw.severity, config.minSeverity)) continue
if (!meetsSeverity(raw.severity, config.minSeverity)) {
dropped.severity++
continue
}

const file = resolveFile(raw.file, files)
if (!file) {
log.debug(`diff에 없는 파일이라 버린다: ${raw.file}`)
dropped.file++
continue
}

Expand All @@ -275,7 +293,10 @@ export function prepareFindings(
}

const key = dedupeKey(finding.file, finding.line, finding.title)
if (seen.has(key)) continue
if (seen.has(key)) {
dropped.duplicate++
continue
}
seen.add(key)
candidates.push(finding)
}
Expand All @@ -289,6 +310,16 @@ export function prepareFindings(
else inline.push(finding)
}

if (result.findings.length > 0) {
const total = dropped.severity + dropped.file + dropped.duplicate
log.info(
`지적 ${result.findings.length}건 → 인라인 ${inline.length}, 요약 ${overflow.length}` +
(total
? ` (제외 ${total} — 심각도 ${dropped.severity}, 경로 ${dropped.file}, 중복 ${dropped.duplicate})`
: ''),
)
}

return { inline, overflow }
}

Expand Down