- P&ID: 연결 분석 API, Prefix 규칙 관리, 카테고리 분류, DXF 그래프 빌드 - LLM: 대화 요약, tool card 영구 보존, 시계열 차트(uPlot), 에이전트 모드 - KB: 청크 미리보기, Field Instrument Inference, 인증/Qdrant 클라이언트 - MCP: 서버 기능 확장, 파이프라인 수정, timeout 개선 - Frontend: P&ID UI, LLM UI, KB UI, OPC UA Write 탭 추가 - 설정: AGENTS.md, plant_context, README, opencode.json 업데이트 - 정리: 진단 체크리스트 문서 삭제
500 lines
21 KiB
Markdown
500 lines
21 KiB
Markdown
# OllamaController.cs — 재진단 보고서 (2차)
|
|
|
|
**진단 대상**: `src/Web/Controllers/OllamaController.cs` (1,194줄)
|
|
**진단 기준**: `diagnosis-checklist.md` 8단계
|
|
**날짜**: 2026-05-16
|
|
|
|
> ⚠️ **1차 진단 대비 주요 변경사항**:
|
|
> - `LoadConfig()` case-sensitivity 버그 발견 (1차 진단에서 누락)
|
|
> - 기존 static 캐시 문제 (HIGH) → 실제 배포 환경(systemd 단일 프로세스)에선 문제 없음으로 재평가 → **MED 강등**
|
|
> - 기존 Headers.Append → **LOW 강등** (현재 아키텍처에선 중복 조건 없음)
|
|
> - 기존 silent catch (HIGH) → 실제 낙하 경로 분석 결과 **MED**로 조정
|
|
> - 신규 발견: `VllmChatStreamWithTools` Reflection 추출, `GetModels` thundering herd
|
|
> - 파일 I/O blocking 항목 → Q3/Q4 탈락으로 보고서에서 완전 제거
|
|
|
|
---
|
|
|
|
## STEP 1 — 맥락 파악 (변경 없음)
|
|
|
|
- **역할**: Ollama / vLLM LLM HTTP Proxy + MCP Tool Calling Bridge (Web/API Controller)
|
|
- **주요 엔드포인트**: `GET /models`, `POST /chat`, `POST /chat/stream` (Ollama native) + `vllm/` variants + `GET/POST /config` + `GET /ping`
|
|
|
|
## STEP 2 — 구조 탐색 (변경 없음)
|
|
|
|
## STEP 3 — 코드 읽기 (전체 재확인 완료)
|
|
|
|
## STEP 4 — 호출 계층 지도 (변경 없음)
|
|
|
|
---
|
|
|
|
## STEP 5 — 패턴 매칭 + STEP 6 — 교차 검증
|
|
|
|
| # | 발견 | Q1 이미 수정? | Q2 다른 레이어? | Q3 의도적? | Q4 재현 시나리오? | 결과 |
|
|
|---|------|:---:|:---:|:---:|:---:|:---:|
|
|
| 1 | **LoadConfig() case-sensitive deserialization** → 저장한 설정이 유실됨 | ❌ | ❌ | ❌ | ✅ 저장 → 새로고침 → 기본값(localhost) 표시 | 🔴 HIGH |
|
|
| 2 | **`VllmChatStreamWithTools` anonymous type reflection 추출** → 느리고 fragile | ❌ | ❌ | ❌ | ✅ 동시 다중 tool_calls + 부하 상황 | 🟠 MED |
|
|
| 3 | **JSON 텍스트 도구 감지 실패 시 silent catch + fallthrough** | ❌ | ❌ | ❌ | ✅ LLM이 `{"tool":"run_sql","parameters":{...}}` 출력 → 파싱 예외 → 도구 무시 | 🟠 MED |
|
|
| 4 | **static 캐시(`_capsCache`) 메모리 누수 — TTL 만료 항목 미제거** | ❌ | ❌ | ❌ | ✅ Ollama에 100+ 모델 등록 시 누적 | 🟠 MED |
|
|
| 5 | **`GetModels()` — 모든 모델 capabilities를 동시 병렬 조회 (thundering herd)** | ❌ | ❌ | ❌ | ✅ Ollama에 20+ 모델 존재 시 /api/show 동시 20+ 요청 | 🟠 MED |
|
|
| 6 | **Summarize — `LoadVllmModel()` 무시하고 환경변수만 참조** | ❌ | ❌ | ❌ | ✅ `VLLM_MODEL` 미설정 + `req.Model` null → 빈 문자열로 vLLM 요청 | 🟠 MED |
|
|
| 7 | **`Response.Headers.Append()` — 이론적 중복 가능** | ❌ | ✅ 현재 미들웨어 체인에서 선행 설정 없음 | ❌ | ❌ 실제 재현 불가(LOW 조건) | 🟡 LOW |
|
|
| 8 | **HttpRequestMessage/HttpResponseMessage 미처분** | ❌ | ✅ .NET HttpClient가 내부 관리 | ❌ | ❌ 실제 누수 측정 불가 | 🟡 LOW |
|
|
| 9 | **ExtractFirstJsonObject 문자열 내 brace 미처리** | ❌ | ❌ | ❌ | ✅ 드물지만 LLM output에 `{` 포함 시 파싱 실패 | 🟡 LOW |
|
|
|
|
---
|
|
|
|
## STEP 7 — 상세 진단
|
|
|
|
---
|
|
|
|
### [1]. LoadConfig() case-sensitive deserialization → 설정 저장 후 유실 (🔴 HIGH)
|
|
|
|
**문제**: `SetConfig()`는 `{"host":"10.0.0.50","port":11434}`처럼 **camelCase**로 JSON 파일을 저장하지만, `LoadConfig()`는 `JsonSerializer.Deserialize<OllamaConfig>(json)` (기본 옵션, `PropertyNameCaseInsensitive = false`)로 읽기 때문에 `host` → `Host` 매칭이 실패한다. `OllamaConfig` 클래스에는 `[JsonPropertyName]` 애트리뷰트가 없으므로, 항상 기본값(`localhost:11434`)이 반환된다.
|
|
|
|
**근거**:
|
|
|
|
파일 쓰기 (`SetConfig()` 492-496줄) — JSON 키가 **camelCase**:
|
|
```csharp
|
|
var json = JsonSerializer.Serialize(new
|
|
{
|
|
host = cfg.Host, // ← "host" (camelCase)
|
|
port = cfg.Port // ← "port" (camelCase)
|
|
}, new JsonSerializerOptions { WriteIndented = true });
|
|
System.IO.File.WriteAllText(path, json);
|
|
```
|
|
|
|
파일 읽기 (`LoadConfig()` 47-63줄) — `PropertyNameCaseInsensitive = false` 기본값:
|
|
```csharp
|
|
OllamaConfig LoadConfig()
|
|
{
|
|
var path = OllamaConfigPath;
|
|
if (System.IO.File.Exists(path))
|
|
{
|
|
var json = System.IO.File.ReadAllText(path);
|
|
return JsonSerializer.Deserialize<OllamaConfig>(json) // ← case-sensitive!
|
|
?? new OllamaConfig(); // ← "host" != "Host" → 기본값 반환
|
|
}
|
|
return new OllamaConfig(); // localhost:11434
|
|
}
|
|
```
|
|
|
|
대상 클래스 (`OllamaConfig` 1144-1150줄):
|
|
```csharp
|
|
public class OllamaConfig
|
|
{
|
|
public string Host { get; set; } = "localhost"; // ← PascalCase
|
|
public int Port { get; set; } = 11434; // ← PascalCase
|
|
public string BaseUrl => $"http://{Host}:{Port}";
|
|
}
|
|
```
|
|
|
|
참고 — ASP.NET Core `[FromBody]` 모델 바인딩은 `AddJsonOptions`에서 `PropertyNameCaseInsensitive = true`가 기본값으로 설정되므로, `SetConfig(OllamaConfig cfg)`에서 `{ host: "..." }` 수신은 성공한다. 하지만 `LoadConfig()`가 호출하는 `JsonSerializer.Deserialize<T>()`는 그 옵션을 공유하지 않는다.
|
|
|
|
**영향**:
|
|
사용자 시나리오:
|
|
1. 설정 화면에서 Ollama host를 `10.0.0.50`로 변경 → `POST /api/ollama/config` → `SetConfig` → 파일 저장 직후 응답은 `{"host":"10.0.0.50","port":11434}`로 정상
|
|
2. 프론트엔드 alert: "변경 사항 적용을 위해 페이지를 새로고침하세요."
|
|
3. 새로고침 → `llmLoadConfigToUI()` → `GET /api/ollama/config` → `GetConfig()` → `LoadConfig()` → `"host"` != `"Host"` → `Host = "localhost"` 반환
|
|
4. 사용자는 "왜 저장이 안 되지?" 반복 시도 → 설정 저장은 항상 실패한 것처럼 보임
|
|
5. 심지어 서버 재시작 후에는 자동으로 `localhost:11434`로 동작하므로, 원격 Ollama 서버에 연결 불가
|
|
|
|
**수정** — 두 가지 중 택일:
|
|
|
|
**수정 A (권장 — `LoadConfig`에서 case-insensitive 옵션 적용):**
|
|
```csharp
|
|
private static readonly JsonSerializerOptions _configJsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
|
|
OllamaConfig LoadConfig()
|
|
{
|
|
try
|
|
{
|
|
var path = OllamaConfigPath;
|
|
if (System.IO.File.Exists(path))
|
|
{
|
|
var json = System.IO.File.ReadAllText(path);
|
|
return JsonSerializer.Deserialize<OllamaConfig>(json, _configJsonOptions)
|
|
?? new OllamaConfig();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "[OllamaController] 설정 로드 실패, 기본값 사용");
|
|
}
|
|
return new OllamaConfig();
|
|
}
|
|
```
|
|
|
|
**수정 B (PascalCase로 파일 저장 — SetConfig 수정):**
|
|
```csharp
|
|
// SetConfig에서 PascalCase로 저장
|
|
System.IO.File.WriteAllText(path, JsonSerializer.Serialize(cfg, new JsonSerializerOptions { WriteIndented = true }));
|
|
```
|
|
|
|
수정 A가 더 안전하다. 언젠가 다른 코드 경로에서도 이 파일을 camelCase로 작성할 가능성이 있으므로, 읽는 쪽에서 대소문자를 무시하는 것이 근본 해결책이다.
|
|
|
|
---
|
|
|
|
### [2]. `VllmChatStreamWithTools` — anonymous type reflection으로 tool call 추출 (🟠 MED)
|
|
|
|
**문제**: `VllmChatStreamWithTools()`(894-899줄)에서 `tcList`에 anonymous object를 담은 후, 같은 메서드 내에서 Reflection(`GetType().GetProperty(...)`)으로 값을 다시 꺼낸다. 값은 이미 변수(`tcId`, `funcName`, `funcArgs`)에 들어 있는데도 중복 추출하고 있다.
|
|
|
|
**근거** (`OllamaController.cs:887-899`):
|
|
```csharp
|
|
// 1. tcList에 anonymous object 저장
|
|
tcList.Add(new
|
|
{
|
|
id = tcId, // ← 이미 이 시점에 값이 로컬 변수에 있음
|
|
type = "function",
|
|
function = new
|
|
{
|
|
name = funcName,
|
|
arguments = funcArgs
|
|
}
|
|
});
|
|
|
|
// 2. 직후에 Reflection으로 다시 꺼냄 (894-899줄)
|
|
foreach (var tc in tcList)
|
|
{
|
|
var tcId = tc.GetType().GetProperty("id")?.GetValue(tc) as string ?? "";
|
|
var func = tc.GetType().GetProperty("function")?.GetValue(tc);
|
|
var funcName = func?.GetType().GetProperty("name")?.GetValue(func) as string ?? "";
|
|
var funcArgs = func?.GetType().GetProperty("arguments")?.GetValue(func) as string ?? "{}";
|
|
```
|
|
|
|
**영향**:
|
|
- Reflection은 직접 접근보다 **10~100배 느림**
|
|
- 컴파일 타임 타입 안전성 없음 — anonymous type property가 rename되면 조용히 `null` 반환
|
|
- 고부하 다중 tool_calls 시 불필요한 CPU 낭비
|
|
|
|
**수정** — 이미 알고 있는 변수 직접 사용:
|
|
```csharp
|
|
// messages.Add용으로만 tcList 유지 (line 887-892)
|
|
messages.Add(new
|
|
{
|
|
role = "assistant",
|
|
content = (string?)null,
|
|
tool_calls = tcList
|
|
});
|
|
|
|
// tool execution은 변수 직접 사용 (894-899줄 대신)
|
|
// 위쪽 루프(866-884)에서 tcId, funcName, funcArgs를 이미 알고 있음
|
|
// → 해당 루프 내에서 바로 tool 실행하거나, 별도 List<(string, string, string)>에 저장
|
|
var toolCallInfos = new List<(string id, string name, string args)>();
|
|
foreach (var tc in toolCalls.EnumerateArray())
|
|
{
|
|
var id = tc.GetProperty("id").GetString() ?? $"tc_{toolRound}_{Guid.NewGuid():N}";
|
|
var func = tc.GetProperty("function");
|
|
var name = func.GetProperty("name").GetString() ?? "";
|
|
var args = func.GetProperty("arguments").GetString() ?? "{}";
|
|
toolCallInfos.Add((id, name, args));
|
|
}
|
|
|
|
// messages에 assistant + tool_calls 추가 (tcList는 anonymous array)
|
|
messages.Add(new { role = "assistant", content = (string?)null, tool_calls = tcList });
|
|
|
|
// tool execution: toolCallInfos의 값을 직접 사용 (Reflection 불필요)
|
|
foreach (var (tcId, funcName, funcArgs) in toolCallInfos)
|
|
{
|
|
await EmitToolStart(tcId, funcName, funcArgs);
|
|
// ... 나머지 동일 ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### [3]. JSON 텍스트 도구 감지 silent catch + fallthrough (🟠 MED)
|
|
|
|
**문제**: `VllmChatStreamWithTools()`(944-1036줄)에서 LLM 응답 내 JSON을 파싱하여 텍스트 기반 도구 호출을 감지할 때, `catch { }`(1035줄)로 예외를 삼킨 후 코드가 아래로 낙하한다. `stopContent`가 있으면 raw text로 SSE 발송되어 사용자에게 도구 의도가 노출된다. 로깅도 없어 디버깅이 불가능하다.
|
|
|
|
**근거** (`OllamaController.cs:944-1036`):
|
|
```csharp
|
|
var jsonCandidate = ExtractFirstJsonObject(stopContent);
|
|
|
|
if (!string.IsNullOrEmpty(jsonCandidate))
|
|
{
|
|
try
|
|
{
|
|
// ... JSON 파싱, 도구 감지, 실행 ...
|
|
if (detectedTool != null && args.Count > 0)
|
|
{
|
|
// ... 도구 호출 ...
|
|
continue; // ← 성공만 continue
|
|
}
|
|
// (실패: detectedTool == null || args.Count == 0) → fallthrough!
|
|
}
|
|
catch { } // ← 파싱 예외도 fallthrough → 로깅 없음
|
|
}
|
|
|
|
// 1038-1047: stopContent가 있으면 raw text로 SSE 발송
|
|
if (!string.IsNullOrEmpty(stopContent))
|
|
{
|
|
var msgJson = JsonSerializer.Serialize(new { message = new { content = stopContent } });
|
|
await Response.WriteAsync($"event: message\ndata: {msgJson}\n\n");
|
|
// ...
|
|
return; // ← 도구 대신 원본 텍스트 노출!
|
|
}
|
|
```
|
|
|
|
**영향**:
|
|
- LLM이 `{"tool": "run_sql", "parameters": {"sql": "..."}}`를 출력했으나 JSON 파싱 실패 → SQL이 실행되지 않고 JSON 텍스트가 그대로 사용자에게 전달됨
|
|
- 에이전트 모드 다단계 추론 중단
|
|
- `catch { }`로 디버깅 불가
|
|
|
|
**수정**:
|
|
```csharp
|
|
if (!string.IsNullOrEmpty(jsonCandidate))
|
|
{
|
|
bool toolExecuted = false;
|
|
try
|
|
{
|
|
// ... 기존 파싱/도출/실행 로직 ...
|
|
if (detectedTool != null && args.Count > 0) toolExecuted = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "[OllamaController] JSON 도구 감지 파싱 실패, Text로 fallback: {Candidate}", jsonCandidate);
|
|
}
|
|
if (toolExecuted) continue;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### [4]. `_capsCache` TTL 만료 항목 미제거 — 메모리 누수 (🟠 MED)
|
|
|
|
**문제**: `GetModelCapabilitiesAsync()`(329-357줄)의 `_capsCache`는 `DateTime.UtcNow.AddMinutes(5)`로 TTL을 설정하지만, 만료된 항목을 제거하는 로직이 없다. Ollama 서버에 새 모델이 계속 추가되면(실험/개발 환경) 딕셔너리가 무한히 커진다.
|
|
|
|
**근거** (`OllamaController.cs:329-357`):
|
|
```csharp
|
|
private static readonly Dictionary<string, (DateTime Until, string[] Caps)> _capsCache = new();
|
|
private static readonly object _capsCacheLock = new();
|
|
|
|
private async Task<string[]> GetModelCapabilitiesAsync(string baseUrl, string model)
|
|
{
|
|
lock (_capsCacheLock)
|
|
{
|
|
if (_capsCache.TryGetValue(model, out var hit) && hit.Until > DateTime.UtcNow)
|
|
return hit.Caps;
|
|
}
|
|
// ... HTTP call ...
|
|
lock (_capsCacheLock) { _capsCache[model] = (DateTime.UtcNow.AddMinutes(5), caps); }
|
|
return caps;
|
|
}
|
|
```
|
|
|
|
**영향**:
|
|
- 10개 모델에서 200개 모델로 증가 시 캐시가 20배 확장 (무제한)
|
|
- 앱 생명주기 동안 제거되지 않아 메모리 단편화 유발
|
|
|
|
**수정** — `IMemoryCache`로 교체 (TTL + 자동 pruning):
|
|
```csharp
|
|
private readonly IMemoryCache _memoryCache;
|
|
|
|
// DI: Program.cs에서 builder.Services.AddMemoryCache();
|
|
public OllamaController(..., IMemoryCache memoryCache)
|
|
{
|
|
_memoryCache = memoryCache;
|
|
}
|
|
|
|
private async Task<string[]> GetModelCapabilitiesAsync(string baseUrl, string model)
|
|
{
|
|
var cacheKey = $"caps_{model}";
|
|
if (_memoryCache.TryGetValue(cacheKey, out string[] cached))
|
|
return cached;
|
|
|
|
try
|
|
{
|
|
// ... HTTP call ...
|
|
_memoryCache.Set(cacheKey, caps, TimeSpan.FromMinutes(5));
|
|
return caps;
|
|
}
|
|
catch { return Array.Empty<string>(); }
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### [5]. `GetModels()` — 모든 모델의 `/api/show`를 동시 병렬 호출 (🟠 MED)
|
|
|
|
**문제**: `GetModels()`(300-305줄)는 `allModels.Select(async n => ...)` + `Task.WhenAll(tasks)`를 사용하여 모든 모델의 capabilities를 **동시에** 조회한다. Ollama 서버에 20개 모델이 있으면 20개의 `/api/show` 요청이 순간적으로 폭주한다.
|
|
|
|
**근거** (`OllamaController.cs:300-305`):
|
|
```csharp
|
|
var tasks = allModels.Select(async n =>
|
|
{
|
|
var caps = await GetModelCapabilitiesAsync(cfg.BaseUrl, n);
|
|
return (name: n, isChat: caps.Contains("completion"));
|
|
}).ToList();
|
|
var results = await Task.WhenAll(tasks);
|
|
```
|
|
|
|
**영향**:
|
|
- Ollama 서버에 부하 집중 (특히 모델이 디스크에서 로딩 중이면 응답 지연)
|
|
- 초기 페이지 로드 시 `/api/ollama/models`가 수십 초 지연 가능
|
|
- 분당 수백 번 호출 시 서버 리소스 고갈
|
|
|
|
**수정** — `SemaphoreSlim`으로 동시성 제한:
|
|
```csharp
|
|
private static readonly SemaphoreSlim _modelCapSemaphore = new(3); // 최대 3 concurrent
|
|
|
|
var tasks = allModels.Select(async n =>
|
|
{
|
|
await _modelCapSemaphore.WaitAsync();
|
|
try
|
|
{
|
|
var caps = await GetModelCapabilitiesAsync(cfg.BaseUrl, n);
|
|
return (name: n, isChat: caps.Contains("completion"));
|
|
}
|
|
finally { _modelCapSemaphore.Release(); }
|
|
}).ToList();
|
|
var results = await Task.WhenAll(tasks);
|
|
```
|
|
|
|
또는 더 간단한 방법 — 캐시가 있으므로 `GetModels`의 `Task.WhenAll`을 제거하고 순차 조회:
|
|
```csharp
|
|
// 이미 5분 TTL 캐시가 있으므로, 순차 조회해도 두 번째 요청부터는 캐시 히트
|
|
foreach (var n in allModels)
|
|
{
|
|
var caps = await GetModelCapabilitiesAsync(cfg.BaseUrl, n);
|
|
if (caps.Contains("completion")) chatModels.Add(n);
|
|
else embeddingModels.Add(n);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### [6]. Summarize — `LoadVllmModel()` 무시 (🟠 MED)
|
|
|
|
**문제**: `Summarize()`(663줄)에서 model 기본값을 `Environment.GetEnvironmentVariable("VLLM_MODEL") ?? ""`로 설정한다. `LoadVllmModel()` 메서드가 파일→환경변수→기본값 순서로 폴백하는 것과 달리, 환경변수만 확인하고 빈 문자열로 폴백한다.
|
|
|
|
**근거** (`OllamaController.cs:663`):
|
|
```csharp
|
|
var model = string.IsNullOrWhiteSpace(req.Model)
|
|
? Environment.GetEnvironmentVariable("VLLM_MODEL") ?? "" // ← LoadVllmModel() 무시
|
|
: req.Model;
|
|
```
|
|
동일 컨트롤러의 기존 메서드 (`OllamaController.cs:536-553`):
|
|
```csharp
|
|
string LoadVllmModel()
|
|
{
|
|
// 1. llm-model.json 파일 확인
|
|
// 2. 파일 없으면 → "Qwen3.6-27B-FP8" 기본값 반환
|
|
}
|
|
```
|
|
|
|
**영향**:
|
|
- `VLLM_MODEL` env 미설정 환경에서 `req.Model` 미포함 요청 시 빈 문자열 model로 vLLM 400 오류
|
|
- llm-model.json에 설정된 모델명이 무시됨
|
|
|
|
**수정**:
|
|
```csharp
|
|
var model = string.IsNullOrWhiteSpace(req.Model)
|
|
? LoadVllmModel() // ← 기존 메서드 재사용
|
|
: req.Model;
|
|
```
|
|
|
|
---
|
|
|
|
### [7]. `Response.Headers.Append()` — 이론적 중복 가능 (🟡 LOW)
|
|
|
|
**문제**: SSE 스트리밍 엔드포인트(402-405, 702-705줄)에서 `Response.Headers.Append()`로 헤더를 설정한다. `IHeaderDictionary.Append()`는 기존 헤더가 이미 존재할 경우 **값을 추가**하므로(덮어쓰지 않음), nginx 등 reverse proxy에서 중복 헤더 충돌 가능성이 있다.
|
|
|
|
**근거** (`OllamaController.cs:402-405`):
|
|
```csharp
|
|
Response.Headers.Append("Content-Type", "text/event-stream");
|
|
Response.Headers.Append("Cache-Control", "no-cache");
|
|
Response.Headers.Append("Connection", "keep-alive");
|
|
Response.Headers.Append("X-Accel-Buffering", "no");
|
|
```
|
|
|
|
**영향**: 현재 아키텍처(Kestrel 직접 서빙, 미들웨어 체인에 선행 헤더 세터 없음)에서는 재현 불가. nginx-proxy 도입 시 발생 가능.
|
|
|
|
**수정**:
|
|
```csharp
|
|
Response.Headers["Content-Type"] = "text/event-stream";
|
|
Response.Headers["Cache-Control"] = "no-cache";
|
|
Response.Headers["Connection"] = "keep-alive";
|
|
Response.Headers["X-Accel-Buffering"] = "no";
|
|
```
|
|
|
|
---
|
|
|
|
### [8]. HttpRequestMessage / HttpResponseMessage 미처분 (🟡 LOW)
|
|
|
|
**문제**: 스트리밍 메서드들(`OllamaChatStream` 420-425줄, `VllmChatStreamSimple` 751-756줄, `VllmChatStreamWithTools` 832-836, 1060-1065줄)에서 `HttpRequestMessage`와 `HttpResponseMessage`에 `using`이 누락되었다.
|
|
|
|
**근거**: `OllamaChatStream`(420-434줄):
|
|
```csharp
|
|
var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"{cfg.BaseUrl}/api/chat")
|
|
{
|
|
Content = content
|
|
}; // ← using 없음
|
|
var res = await _httpClient.SendAsync(httpRequest, ...); // ← using 없음
|
|
// ...
|
|
using var reader = new StreamReader(stream, Encoding.UTF8); // StreamReader만 using
|
|
```
|
|
|
|
**영향**: .NET `HttpClient`(SocketsHttpHandler)가 내부적으로 연결을 관리하고, `StreamReader` disposal이 스트림을 닫으므로 실질적 누수는 미미하다. 하지만 코드 분석 툴에서 경고를 발생시키고, 극단적 부하에서 GC 압박 유발 가능.
|
|
|
|
**수정**: 일관성 있는 `using var` 적용:
|
|
```csharp
|
|
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, ...) { Content = content };
|
|
using var res = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted);
|
|
```
|
|
|
|
---
|
|
|
|
### [9]. `ExtractFirstJsonObject` — 문자열 내 `{` brace 미처리 (🟡 LOW)
|
|
|
|
**문제**: `ExtractFirstJsonObject`(1130-1141줄)가 escaping과 문자열 상태를 고려하지 않고 depth만 센다. LLM 응답 문자열 리터럴에 `{`가 포함되면 depth 계산이 틀어진다.
|
|
|
|
**영향**: LLM이 응답 중간에 JSON 형식을 출력하면(예: `설명: 매개변수 {a, b, c}`) brace depth가 깨져서 잘못된 범위 추출. 발생 빈도 낮음.
|
|
|
|
**수정**: 1차 진단과 동일 (문자열 상태 추적 로직 추가).
|
|
|
|
---
|
|
|
|
## STEP 8 — 자가 검증
|
|
|
|
- [x] 각 지적 사항을 현재 파일의 특정 줄 번호로 직접 가리킴
|
|
- [x] HIGH 항목(1건)은 재현 가능한 시나리오를 한 문장으로 서술 가능
|
|
- [x] 교차 검증 4개 질문 모두 통과
|
|
- [x] 실측 없는 성능 수치 배제
|
|
- [x] 기존 보고서 누락 항목(`LoadConfig` case-sensitivity) 추가 완료
|
|
|
|
---
|
|
|
|
## 최종 요약
|
|
|
|
| 심각도 | 수 | 항목 |
|
|
|--------|---|------|
|
|
| 🔴 HIGH | 1 | [1] `LoadConfig()` case-sensitive deserialization → 설정 저장 후 유실 |
|
|
| 🟠 MED | 5 | [2] Reflection tool call 추출, [3] JSON 감지 silent catch + fallthrough, [4] 캐시 메모리 누수, [5] Thundering herd, [6] Summarize 기본값 누락 |
|
|
| 🟡 LOW | 3 | [7] Headers.Append 중복, [8] HttpMessage disposal 누락, [9] ExtractFirstJsonObject brace |
|
|
|
|
**우선 수정 순서**: [1] → [2] → [3] → [5] → [4] → [6] → [9] → [7] → [8]
|
|
|
|
- **[1]은 서비스 장애** — 설정을 저장할 수 없어 원격 Ollama 연결 불가
|
|
- **[2]는 기능 버그** — Reflection 깨지면 tool_calling 무시
|
|
- **[3]은 디버깅 불가** — 도구 호출 누락 시 silent
|
|
- **[5]는 성능** — 모델 20개 이상 시 페이지 로드 수초 지연
|
|
- **[4][6][9]는 안정성/일관성**
|
|
- **[7][8]은 Best practice**
|
|
|
|
### 1차 보고서와의 비교
|
|
|
|
| 항목 | 1차 (등급) | 2차 (등급) | 사유 |
|
|
|------|:---------:|:---------:|------|
|
|
| static 캐시 | 🔴 HIGH | 🟠 MED | systemd 단일 프로세스 환경에선 문제 아님. 메모리 누수만 재평가 |
|
|
| JSON silent catch | 🔴 HIGH | 🟠 MED | fallthrough 후에도 stopContent는 SSE 발송됨 (crash는 아님) |
|
|
| Headers.Append | 🟠 MED | 🟡 LOW | 선행 미들웨어 없어 실제 재현 불가 |
|
|
| HttpMessage disposal | 🟠 MED | 🟡 LOW | .NET 런타임이 내부 관리 |
|
|
| LoadConfig 캐싱 | 🟡 LOW | **🔴 HIGH** | **case-sensitivity 버그 발견** (1차 누락) |
|
|
| Reflection tool call | — | 🟠 MED | 1차 누락 |
|
|
| Thundering herd | — | 🟠 MED | 1차 누락 |
|
|
| Summarize 기본값 | 🟠 MED | 🟠 MED | 유지 |
|
|
| ExtractFirstJsonObject | 🟡 LOW | 🟡 LOW | 유지 |
|