# Ruo Lee — Notes

> Design notes from an AI engineer. Not what was built, but why it was decided that way.

Engineering notes by Ruo Lee (이루오), an AI engineer. Every post is a decision record: a problem that was measured, the options that were weighed, what was chosen, and what is still unsolved. Numbers come from production systems the author built or operates. Each post exists in Korean and English at the same slug — `/ko/<slug>` and `/en/<slug>`.

## Posts (Korean)

- [오픽 답변의 약점 태그를 Jev로 판정하기까지](https://blog.po24lio.com/ko/opic-weakness-tags-with-jev.md): 평가에서 Gemini에 9 대 7로 진 Jev를 판정기로 고르고, 질문을 다시 써서 정답률을 70%에서 93~97%로 올린 일. 그리고 받아쓰기가 지어낸 답을 Jev가 못 잡는 이유. (오픽 앱 · 2026-10-04)
- [세계일주 사이트에 Jev를 붙인 이틀](https://blog.po24lio.com/ko/jev-on-the-worldtrip-site.md): 사진 1,944장에 Jev로 주제를 붙이고, 아무 말이나 치면 지구본이 그 정거장으로 가는 검색을 만든 10월 2일과 3일. 판정선, 결과 0, 사진 먼저, 비용, 로컬에서만 통과한 배포. (세계일주 사이트 · 2026-10-03)
- [Jev는 LLM과 무엇이 다른가](https://blog.po24lio.com/ko/jev-is-not-an-llm.md): 문장을 쓰지 않고 정해 둔 답에 확률만 매기는 모델 Jev를 LLM과 나란히 놓고 공부한 기록. 한 번에 매기는 구조, 사진을 보지 않고 글을 읽는다는 것, 0.96이 96%라는 뜻인지. (세계일주 사이트 · 2026-10-02)
- [사주할매 제작기 (1) 만세력 엔진 고르기부터 심사 제출까지](https://blog.po24lio.com/ko/saju-halmae-making-of-1.md): 만세력은 코드로 계산하고 긴 풀이는 AI가 할매 목소리로 쓰는 사주 앱. 9월 23일 밤 빈 레포에서 25일 심사 제출까지. (사주할매 · 2026-10-02)
- [allowed_agent_ids 가 worker_agent_ids 가 되기까지](https://blog.po24lio.com/ko/allowed-to-worker-agent-ids.md): A2A 오케스트레이터의 워커 목록으로 호출까지 막았다가 사흘 만에 뺐다. 고객사 요구에 미리 대비한 통제였는데, 외부에서도 부를 수 있어야 한다는 지금의 요구와 맞지 않았다. (동적 실행 엔진 · 2026-09-23)
- [목적별 에이전트를 A2A 워커로 쪼갠 오케스트레이터](https://blog.po24lio.com/ko/splitting-agents-into-workers.md): RAG, 대용량 첨부 검색, 표 검색을 에이전트마다 따로 만들던 것을, 잘게 나눈 워커를 오케스트레이터에 도구로 쥐여 주는 방식으로 바꿨다. 개발 클러스터 테스트에서 엉뚱한 워커를 고른 적은 없었고, 고칠 곳은 워커 단위로 갈렸다. (동적 실행 엔진 · 2026-09-23)
- [사람들은 저런 거 잘 안 읽어](https://blog.po24lio.com/ko/nobody-reads-the-banner.md): 저장된 설문이 부족한 사용자를 안내 문구로 유도하려다 실패하고, 권장 동작을 주버튼 자리에 놓는 것으로 바꿨다. 그 뒤로 인터랙션 규칙을 프로젝트 규칙으로 들였고, 규칙으로 화면을 다시 보니 드러난 것들. (오픽 앱 · 2026-09-21)
- [가입이 27% 줄었는데 매출은 최고였던 주](https://blog.po24lio.com/ko/signups-down-revenue-up.md): 주말 가입이 급감해서 사고인 줄 알고 들어갔다. 시장 수요의 계절 하강이었고, 코호트를 뽑아 보니 리텐션으로 버틸 사업이 아니었다. (오픽 앱 · 2026-09-21)
- [지도 인셋 규칙을 실측으로 정하기](https://blog.po24lio.com/ko/eyeballed-then-justified.md): 인셋 지도를 언제 띄울지 정거장 155개를 전부 돌려서 정했다. 예외는 브뤼셀 하나였고, 104px이라는 크기는 끝까지 눈대중으로 남았다. (지도 인터페이스 · 2026-09-13)
- [R8 최적화가 결제 플러그인을 지웠다](https://blog.po24lio.com/ko/everything-but-the-payment-test.md): Play Console 요구대로 R8 을 켰더니 Cordova 결제 플러그인 등록부가 지워졌다. 앱 결제가 열흘 동안 0건이었고, keep 규칙 한 줄로 고쳤다. (오픽 앱 · 2026-09-11)
- [moof 로 시작하는 파일](https://blog.po24lio.com/ko/blamed-ios-twice.md): 녹음 시작 함수의 await 구간에 재진입이 생겨 초기화 세그먼트가 버려졌다. iOS 탓으로 보고 고쳤는데 소용이 없었고, 업로드 파일의 첫 바이트를 찍고서야 원인이 나왔다. (오픽 앱 · 2026-09-09)
- [UTC 서버에서 생긴 KST 경계 버그 여섯 개](https://blog.po24lio.com/ko/where-is-midnight.md): 서버는 UTC 로 돌고 사용자의 하루는 KST 로 시작한다. 증상이 전혀 안 닮은 버그 여섯 개가 같은 뿌리였고, 두 기준을 한 함수 안에서 섞은 게 문제였다. (오픽 앱 · 2026-09-08)
- [다계정 파밍 피해액을 확인해 보니 $0.27](https://blog.po24lio.com/ko/measuring-abuse-then-doing-nothing.md): 대책을 고르기 전에 피해 규모부터 확인했다. 피해액이 $0.27 이라 아무것도 하지 않기로 했고, 대신 오히려 문제를 만들던 기존 방어 하나를 걷어냈다. (오픽 앱 · 2026-09-07)
- [오픈소스 13,000줄을 벤더링하기로 한 이유](https://blog.po24lio.com/ko/vendoring-someone-elses-code.md): 의존성으로도 포크로도 안 돼서 벤더링을 골랐다. 대가로 포매터를 대지 않고 커밋을 나누는 규율이 생겼고, 못 고치는 것은 문서에 적어 뒀다. (에이전트 메모리 · 2026-09-05)
- [Mount 접두어 매칭이 라우트를 가릴 때](https://blog.po24lio.com/ko/mounted-but-unreachable.md): 배포도 성공했고 라우트도 등록됐는데 호출은 404 였다. FastAPI 의 Mount 가 접두어로 첫 매치를 쓰기 때문이고, 별칭에 슬래시를 허용한 것이 원인이었다. (동적 실행 엔진 · 2026-09-01)
- [예외를 던지지 않는 실패들](https://blog.po24lio.com/ko/failures-that-dont-throw.md): 에러 로그를 남기지 않는 결함 여섯 개를 찾았다. 빈 배열 하나로 "없음"과 "못 가져옴"을 함께 표현한 게 공통점이었고, 넷은 배포 전에 잡았다. (에이전트 메모리 · 2026-08-29)
- [문장 쌍을 모아 유사도 임계값 0.85를 정한 과정](https://blog.po24lio.com/ko/thresholds-are-measured.md): 0.9로 시작했다가 문장 쌍을 모아 실제 임베딩 모델로 재서 0.85로 정했다. 두 방향의 실패 비용이 달라 여유는 한쪽으로 몰았고, 검색 경로에서는 임계를 아예 버렸다. (에이전트 메모리 · 2026-08-22)
- [한국어 대화에서 영어 기억이 추출되던 이유](https://blog.po24lio.com/ko/examples-beat-instructions.md): 추출 프롬프트에 영어 few-shot 예시가 14개 있었다. 지시문을 어디에 넣어도 안 바뀌었고, 예시를 3개로 줄이고 하나를 한국어 쌍으로 바꾸자 해결됐다. (에이전트 메모리 · 2026-08-15)
- [채널 키를 요청 ID로 잡으면 안 되는 이유](https://blog.po24lio.com/ko/a-channel-named-after-the-wrong-thing.md): Redis 채널 이름을 요청 ID로 지어서 중첩 호출 때 구독자가 둘이 됐다. 토큰 5개가 195개로 불어났고, 몇 주 뒤에는 같은 원인으로 병렬 호출의 토큰이 섞였다. (동적 실행 엔진 · 2026-03-13)
- [SSE 프레임 파싱을 하루 동안 고친 기록](https://blog.po24lio.com/ko/touching-someone-elses-stream.md): 24시간 동안 같은 열 줄을 고치고 또 고쳤다. 아무것도 잃지 않은 원시 통과 버전은 두 시간이 안 돼 지워졌고, 그렇게 자주 손을 탄 건 그 코드가 생성되는 문자열 안에 있었기 때문이다. (동적 실행 엔진 · 2025-12-12)
- [사용자 코드를 런타임에 import 하는 엔진](https://blog.po24lio.com/ko/filesystem-instead-of-a-database.md): 사용자 파이썬을 재시작 없이 API 로 만들려면 그 코드가 디스크의 진짜 파일이어야 한다. 그 제약에서 나온 버그 세 종류와, 땜빵 세 개 뒤에 바꾼 방향을 적었다. (동적 실행 엔진 · 2025-10-31)

## Posts (English)

- [Choosing Jev to tag weaknesses in OPIc answers](https://blog.po24lio.com/en/opic-weakness-tags-with-jev.md): Picking Jev as the judge after it lost to Gemini 9 to 7, rewriting the questions to take accuracy from 70% to 93–97%, and why Jev can’t catch an answer the transcription made up. (OPIc app · 2026-10-04)
- [Two days of putting Jev into my world trip site](https://blog.po24lio.com/en/jev-on-the-worldtrip-site.md): October 2 and 3: tagging 1,944 photos with Jev, and a search where you type anything and the globe flies to that stop. Drawing the lines, zero results, photos first, cost, and a deploy that only passed locally. (World trip site · 2026-10-03)
- [How Jev differs from an LLM](https://blog.po24lio.com/en/jev-is-not-an-llm.md): Notes from studying Jev, a model that writes no text and only puts probabilities on answers you define, side by side with an LLM: scoring in one pass, reading text instead of photos, and whether 0.96 means 96%. (World trip site · 2026-10-02)
- [Building Saju Halmae (1): from picking a manseryeok engine to store review](https://blog.po24lio.com/en/saju-halmae-making-of-1.md): A saju app whose calendar maths is plain code and whose long readings are written by AI in a grandmother’s voice. From an empty repo on the night of September 23 to review on the 25th. (Saju Halmae · 2026-10-02)
- [How allowed_agent_ids became worker_agent_ids](https://blog.po24lio.com/en/allowed-to-worker-agent-ids.md): An A2A orchestrator's worker list was used to block calls too, and three days later that came out. It was a guard set up in advance for a customer requirement, and it got in the way of the one in front of us: being callable from outside. (Dynamic execution engine · 2026-09-23)
- [An orchestrator that splits purpose-built agents into A2A workers](https://blog.po24lio.com/en/splitting-agents-into-workers.md): We used to build a separate agent for RAG, for searching large attachments, for table search. Now small workers are handed to one orchestrator as tools. In dev-cluster testing it never picked the wrong worker, and each fix landed on one worker. (Dynamic execution engine · 2026-09-23)
- [People don't read those things](https://blog.po24lio.com/en/nobody-reads-the-banner.md): I tried to steer users with a short survey toward re-selecting by rewriting a banner, and lost. Then I put the recommended action in the primary button's place, adopted a set of interaction rules for the app, and looked at the screens again through them. (OPIc app · 2026-09-21)
- [The week signups fell 27% and revenue hit a high](https://blog.po24lio.com/en/signups-down-revenue-up.md): Weekend signups dropped and I went in expecting an outage. It was the seasonal downswing of the whole market, and once I pulled the cohorts it turned out this was never a retention business. (OPIc app · 2026-09-21)
- [Setting the map inset rule by measurement](https://blog.po24lio.com/en/eyeballed-then-justified.md): I decided when to show the inset map by running all 155 stops. Brussels was the single exception, and the 104px size stayed an eyeball call to the end. (Map interfaces · 2026-09-13)
- [R8 optimisation deleted the payment plugin](https://blog.po24lio.com/en/everything-but-the-payment-test.md): Turning on R8 as Play Console required stripped the Cordova payment plugin registry. In-app payments sat at zero for ten days; one keep rule fixed it. (OPIc app · 2026-09-11)
- [A file that starts with moof](https://blog.po24lio.com/en/blamed-ios-twice.md): Re-entering the start function across its await threw away the initialisation segment. I blamed iOS and fixed for it, which did nothing; printing the first bytes of the upload settled it. (OPIc app · 2026-09-09)
- [Six KST boundary bugs on a UTC server](https://blog.po24lio.com/en/where-is-midnight.md): The server runs on UTC and the user's day starts in KST. Six bugs with nothing in common on the surface shared one root, and the problem was two reference points mixed inside a single function. (OPIc app · 2026-09-08)
- [Multi-account farming cost us $0.27](https://blog.po24lio.com/en/measuring-abuse-then-doing-nothing.md): I checked the scale before picking a countermeasure. At $0.27 the answer was to do nothing, and to remove one existing defence that was causing more harm than it prevented. (OPIc app · 2026-09-07)
- [Why we vendored 13,000 lines of open source](https://blog.po24lio.com/en/vendoring-someone-elses-code.md): Neither a dependency nor a fork worked, so we vendored. The price is a discipline: no formatter over that tree, commits kept separate, and what we cannot fix written down. (Agent memory · 2026-09-05)
- [When Mount prefix matching hides a route](https://blog.po24lio.com/en/mounted-but-unreachable.md): The deploy succeeded, the route was registered, and calls still returned 404. FastAPI's Mount matches on prefix and takes the first hit; allowing a slash in the alias was the cause. (Dynamic execution engine · 2026-09-01)
- [The failures that never throw](https://blog.po24lio.com/en/failures-that-dont-throw.md): Six defects that never raised an error. All of them used one empty array for both "nothing there" and "could not fetch"; four were caught before deploy. (Agent memory · 2026-08-29)
- [Setting the 0.85 similarity threshold from sentence pairs](https://blog.po24lio.com/en/thresholds-are-measured.md): I started at 0.9, then measured real sentence pairs through the production embedding model and landed on 0.85. The two directions of failure cost different amounts, so the slack went to one side; on the search path I dropped the threshold entirely. (Agent memory · 2026-08-22)
- [Why Korean conversations produced English memories](https://blog.po24lio.com/en/examples-beat-instructions.md): The extraction prompt carried 14 English few-shot examples. No placement of an instruction changed anything; cutting to three examples with one Korean pair did. (Agent memory · 2026-08-15)
- [Why a channel key should not be the request ID](https://blog.po24lio.com/en/a-channel-named-after-the-wrong-thing.md): Naming the Redis channel after the request ID gave a nested call two subscribers. Five tokens arrived as 195, and the same cause came back weeks later on parallel calls. (Dynamic execution engine · 2026-03-13)
- [A day of fixing SSE frame parsing](https://blog.po24lio.com/en/touching-someone-elses-stream.md): The same ten lines were rewritten again and again in 24 hours. The raw-passthrough version, the only one that lost nothing, was gone in under two hours — and the code kept changing because it lived inside a generated template string. (Dynamic execution engine · 2025-12-12)
- [An engine that imports user code at runtime](https://blog.po24lio.com/en/filesystem-instead-of-a-database.md): Turning user Python into a live API without a restart means the code has to be a real file on disk. Three classes of bug came out of that, and three patches in I changed direction. (Dynamic execution engine · 2025-10-31)

## Optional

- [Index, Korean](https://blog.po24lio.com/ko): all posts, newest first
- [Index, English](https://blog.po24lio.com/en): all posts, newest first
- [Portfolio](https://ruo.po24lio.com): the author's work and background
- [RSS](https://blog.po24lio.com/rss.xml): feed of recent posts
