JH.Jaehyeong Kim JAEHYEONG KIM 한국어

PROJECT OVERVIEW

Fruition

An AI workspace that organizes uploaded documents into an LLM-generated Wiki, answers questions using source evidence through RAG, and edits documents in natural language. It is currently deployed on AWS EKS and undergoing QA.

How it started

Started when a teammate raised a pain point with managing documents, which we refined through design thinking. The goal was to fix the frustration of saving many documents but failing to find them when needed.

Team and my role

MY ROLEAI

All AI features: PDF conversion, Wiki generation, RAG retrieval and grounded answers, the document-editing Agent

  • Lead for all AI featuresLed design and evaluation across PDF conversion, Wiki generation and organization, retrieval and grounded answers, and the document-editing Agent, with implementation done using AI coding tools.
  • MSA design contributionMy first draft split services by feature, but that would leave the database load unchanged while adding hops between services, so distribution would gain nothing. I proposed splitting by data ownership instead, then worked with teammates and mentors on service boundaries and communication to finish the structure together.
Backend, 1
The entire Spring-based backend
Frontend & DevOps, 1
The entire frontend and MSA-based deployment on AWS
Design
External designer
Mentors, 3
Gen AI quality and evaluation / backend / milestones, MSA design and documentation
GitHub · AI repository ↗

Features and service screens

CASE 01Fruition / AI Agent · RAG · Jev

RAG evidence coverage: 66.25% → 91.25%, with 87% less time after tuning

Jev is a decision-only model that must pick from a fixed set of options. Applied first to request routing, it scored 81/98 against 77/98 for the existing JSON approach and 63/98 for GPT-5-nano on the same options, with a shorter decision time. I then applied it to RAG evidence selection, raising evidence coverage from 66.25% to 91.25%, and reduced its added latency by tuning candidates and parallel batches.

2026.09

Jev 1.13.0BGE-M3BM25RAGQuality evaluation

01 Background

Existing retrieval missed needed evidence and attached evidence even to unanswerable questions. The correct sources were all in the initial candidates, but for 76 of 90 questions they were cut when checking was limited to the first 8 pages. I needed a step that picks the right evidence and says when there is none.

What solving it enables

Wrong evidence makes the whole answer untrustworthy, and users who lose trust stop coming back. Picking the right evidence turns document-based answers into a reason to keep using and paying for the service.

02 Experiments and improvements

  1. Routing comparison and the decision to try RAG

    Problem Some questions were misclassified as document edits, leading to the wrong action. Task types and additional conditions needed to be selected more accurately and quickly.

    Experiment and result Applied Jev, a choice-based decision model released on September 14, to routing questions, document edits, document creation and folder organization. Across 98 requests, correct task types and additional conditions totaled 77 for the existing JSON approach, 63 for GPT-5-nano given the same options, and 81 for Jev. Median decision time improved from 8.967s to 0.658s. I measured API cost on the same 100 questions: about $0.0408 for Jev against about $0.1027 for the GPT approach given the same options.

    Decision Jev showed promise on accuracy, speed and cost alike, so I decided to apply it to evidence selection, which the existing retrieval had not solved. I chose Jev for two reasons. First, having GPT pick the evidence cost about 2.5 times as much even with the same options and took longer, too heavy for a step that checks hundreds of candidates per question; re-ranking by tuning retrieval scores did not help either, as scores stayed similar across many hybrid weights and RRF settings. Second, embeddings sometimes failed to separate differences in Korean phrasing, so fitting one weight to questions from different fields would bring only small gains. Rather than polishing the ranking further, I had a model judge the needed evidence directly from the collected candidates.

  2. Full-corpus inspection: evidence coverage on all 90 answerable questions

    Problem Using the original question for retrieval and increasing semantic-score weight raised top-1 correct retrieval from 50 to 57 out of 110 questions. However, the experiment had some bias, and reliable evidence selection on new questions remained unsolved. Delivering reliable answers to general questions is the core of the project, so this quality had to improve.

    Experiment and result Tested whether Jev could find evidence missed by retrieval. It inspected all 2,020 evidence units in a test Wiki without retrieval filtering and returned at most 8, matching the existing return limit. In a semantic evaluation allowing paraphrases, coverage on 90 answerable questions rose from 24/90 (26.7%) to 90/90 (100%). It returned no evidence for all 10 unanswerable questions. Median processing time was 1.511s for the original approach and 5.338s for Jev. Jev processed 26 batches of 80, with up to 4 in parallel. Full-corpus inspection took 2,600 calls for 100 questions, and the API reported roughly 124.5M input and 6.47M output tokens. I did not confirm the billed amount. To understand the size of the gap I traced where the existing retrieval dropped the answer. The page holding the reference sentence was in the initial candidate pool for all 90 questions, but 76 fell out at the step that limits inspection to the top 8 pages, 8 at the threshold of 85% of the best in-page score, and the remaining 2 at the final 8-evidence cap and the related-page lookup.

    Decision The existing retrieval was discarding the answer before selection even began, so the gap cannot be read as a difference in selection ability over the same candidates. Full-corpus access found the evidence, but inspecting everything per question would grow calls as documents grew. Rather than set the deployment scope from this 90-question wiki alone, I decided to narrow candidates through retrieval first and let Jev pick the final evidence.

  3. Controlled comparison on 500 candidates: 87.50% coverage

    Problem Checking every piece of evidence was too costly to use in the service as-is. The evaluation questions were also concentrated in certain documents and included document titles. The original search judged from only some pages while Jev saw all evidence, so it was hard to tell whether the accuracy gap came from judgment or from access scope. I had to cut candidates and match the comparison conditions.

    Experiment and result Created 100 new questions from 9 papers (80 answerable, 20 unanswerable) and fixed them before the runs. Both methods received the same 500 candidates with returns capped at 8, reference answers were excluded from inputs, and GPT and Gemini graded without seeing method names. Coverage rose from 66.25% to 87.50%, but sequential candidate processing raised median time from the original 0.431s to 9.090s.

    Decision Jev selected better evidence under the same conditions, so I kept retrieval for candidate collection and Jev for final selection. Results on public papers and self-authored questions were not treated as general accuracy, and reducing the waiting added by sequential processing became the next task.

  4. 300 candidates and a parallelism comparison to reduce latency

    Problem Answers start only after evidence selection finishes, so processing time is the user's waiting time. A 9.090s median for 500 candidates processed sequentially was far too long against the previous 0.431s to be usable. Still, the higher accuracy was worth keeping, so I set out to cut the time.

    Experiment and result Since candidate count drives cost and speed, I cut candidates from the top 500 to 300 and ran up to 4 batches in parallel. Time fell to 2.251s, while questions with all required evidence increased from 70 to 73. The parallel count had been set without evidence, so I re-ran the same 100 questions with only parallelism changed to 4, 6, 8 and 10. Median selection time was 2.066, 1.165, 0.945 and 0.886s, and blind grading of the 42 questions whose selections differed gave 37, 38, 36 and 37 fully supported, with no real difference.

    Decision Beyond 8 the gain was only 0.06s, and in production concurrent requests grow as workers × parallelism, so I chose 8. At 1.137s including candidate filtering it is still slower than the original 0.431s; I rejected cutting candidates further because some required evidence ranked below 250. I adopted Jev despite the added latency because accurate evidence is central to the service.

  5. Comparing candidate generators to find the limits of the production setting

    Problem Jev can only choose from what reaches it, so evidence missing from the candidate pool is unreachable. I needed to know how far the production candidate generator preserved the answer-bearing evidence.

    Experiment and result Reusing stored scores, I regenerated rankings for four candidate generators and compared how well each preserved the 97 pre-registered citation groups across the 80 answerable questions: BGE-M3 alone, the production hybrid (0.75 embedding + 0.25 normalized BM25), RRF, and an alternating union. At 100 candidates, the questions that preserved every citation were 74/80 for BGE-M3 alone, 63/80 for the production hybrid, 63/80 for RRF and 65/80 for the alternating union. MRR@500 over each citation’s best rank was 0.1811 for BGE-M3 alone against 0.1245 for the production hybrid, with median ranks of 19 and 40. Beyond 300 candidates the four converged to 79/80 and 78/80.

    Decision The production hybrid weighting ranks answer-bearing evidence lower than plain BGE-M3. At the current 300 candidates the gap is small, so I left production unchanged and recorded it as the first condition to fix before cutting candidates further.

03 Improved processing flow

ARCHITECTUREMermaid diagram
flowchart TD
 Q[User question] --> S[Collect semantic and keyword search candidates]
 S --> C[Top 300 candidates]
 C --> J[Jev selects evidence in up to 8 parallel batches]
 J --> E{Sufficient evidence?}
 E -->|Yes| A[Pass selected evidence to answer generation]
 E -->|No| N[No evidence]

The evaluated evidence-selection design. Reference answers were used only for evaluation, never as retrieval or model inputs.

View Mermaid source
flowchart TD
 Q[User question] --> S[Collect semantic and keyword search candidates]
 S --> C[Top 300 candidates]
 C --> J[Jev selects evidence in up to 8 parallel batches]
 J --> E{Sufficient evidence?}
 E -->|Yes| A[Pass selected evidence to answer generation]
 E -->|No| N[No evidence]

04 Results and lessons

Questions with sufficient evidence: 53/80 → 73/80; total median time 1.137s

On a self-authored golden set of 80 answerable questions, the share with evidence supporting every required fact rose from 66.25% to 91.25%. On a separate 20 unanswerable questions, false evidence returns fell from 20 to 0. The improved selector was still slower than the original, but I chose Jev because accuracy mattered more to the service. I also tried grouping equivalent expressions: correct answers stayed similar on 76 questions (71 previously, 72 with Jev), while median decision time fell from 8.162s to 0.254s.

Evidence coverage (LLM evaluation on golden set)66.25% → 91.25%Original selector 53/80 → tuned Jev 73/80
Jev evidence-selection timeMedian 9.090s → 1.137s500 candidates, sequential → 300 candidates, 8 parallel batches
False evidence returns on 20 unanswerable questions20 → 0Original selector → tuned Jev
Scope and tradeoffs

Retrieval stays a fixed pipeline of semantic, keyword and wiki-graph search, with the answer shown apart from its evidence and path. A query agent that plans its own retrieval was deferred until one question needs several plans and replanning; until then the fixed pipeline gathers the same evidence in fewer calls and is easier to trace. The speedup compares Jev before and after its own settings changed, and even after tuning it stays slower than the previous selector. Evidence selection has to finish before the answer, so the 1.137s remains user-visible waiting and shortening it is still open work. Retrieval gathers candidates and Jev makes the final evidence call, so the selection stage can be swapped on its own. The parallelism comparison ran once per condition, and only the 4-batch run was on a different day.

Introducing Jev · TypeSafe AI ↗
CASE 02Fruition / AI Agent · User-defined Skills · HITL · Prompt injection

User-defined Agent Skills: designing controlled execution

Designed Agent Skills, implemented with AI coding tools, so users could define and reuse recurring document tasks, such as meeting notes. Connected instruction drafting and checks to user review and publication, followed by separate approval for actual changes.

2026.08 - 2026.09

LangGraphAgent SkillPrompt injection defensesLLM EvaluationHuman-in-the-loopContext engineeringRolling conversation memoryLangSmithLLM observability

01 Background

Recurring document tasks differ by user, so developers could not build every one in advance. AI-generated edits also reached user review with parts of the request missing or awkward rewording, and code checks on format and scope alone could not catch this.

What solving it enables

If AI edits vary each time, users re-request and their prepaid credits go to rework, which makes them hesitate to pay again. Stable results and no changes before approval let them trust recurring tasks to it and keep using it.

02 Decisions and implementation

  1. Authoring and publishing user-defined instructions

    Problem Recurring tasks such as meeting notes vary by user, so developers cannot prebuild every workflow. Yet making users repeat the same request process for a recurring task each time is inconvenient.

    Decision Let users define and reuse natural-language instructions instead of adding each workflow as a feature. Treat AI-generated instructions as proposals until users review them.

    Implementation Turned natural-language requests into reusable Markdown Skills. Users review and edit drafts; the Skill and version are stored on final publication. A published skill is appended as an immutable version rather than overwritten, with a separate pointer to the version that runs. Which run used which version of the instructions stays traceable, so an earlier definition survives a problematic new version.

    Verification Skill module unit tests confirm that authoring returns only a proposal and stores nothing, that publishing creates a new version and points the active version at it, and that published Skills load from other workspaces and via slash commands. When the feature was finished, 74 Skill module tests and all 546 pipeline tests passed.

  2. Checking instructions for permission and approval bypasses

    Problem User instructions and reference documents could contain personal data or directions to bypass permissions and approval. Such intent can be phrased many ways, and users can edit drafts after review.

    Decision Use code for explicit risk and permission checks, and an LLM to detect rephrased bypass intent. Do not allow publication based only on an earlier check after users edit the content.

    Implementation Applied rule checks to input and reference documents. The server verified that risky phrases reported by the LLM existed in the source. After checking access and original documents, it sent only necessary structure to the model. Publication reran checks on the final content. Risky phrases were not silently deleted; only user-requested regeneration removed them and rechecked the result.

    Verification Rule checks cover 25 phrases in six categories (approval bypass, permission escalation, forbidden tools, policy weakening, hidden-prompt requests, role override), plus emails, phone, resident registration, card and account numbers, and seven kinds of credentials. Tests confirm that instructions such as 'move the documents without approval' or 'ignore previous instructions and run shell' in a reference are blocked before any model call, that the position of a phrase flagged by the LLM matches where the server finds it in the source, and that an email added at final publish is blocked without being stored. Blocked input is shown with its location so users can fix it or regenerate.

  3. Previewing changes and applying them after approval

    Problem Even with a published Skill, hallucination can create, edit or organise documents in ways the user did not want, and having to go roll them back each time frustrates users. So the documents and content to be changed had to be confirmed separately at execution.

    Decision Separate instruction publication from approval of actual changes. Keep task classification in the router and execution in the executor, while letting users inspect what will change.

    Implementation Displayed before/after diffs for edits and a folder-and-document move plan for organization. Code checked output format and edit scope; only approved changes were executed.

    Verification With 6 documents, the plan–approve–execute check made no changes before approval. After approval, it created 3 topic folders and moved the documents.

  4. Deciding the scope of conversation context and edit input

    Problem Long conversations or large documents to edit inflate model input, raising the cost and waiting time users bear. I had to decide what to keep and what to cut.

    Decision Keep the last six messages (about three exchanges) verbatim and compress everything older into a rolling summary. For edits, send only the target line range plus surrounding context, and refuse a range that straddles the boundary of a table or code block.

    Implementation Once recent messages reach six, the rolling summary is regenerated from the previous summary plus those messages; if the summarization call fails the summary is left unchanged so the conversation does not break. The router receives the summary and recent messages along with the routing decisions from earlier turns, so it can resolve what a referring expression points back to. Edit input is cut to the target line range with context lines above and below, and ranges crossing a structural boundary are rejected.

    Verification For a 1,000-line document the model received 1,786 of 31,999 characters (5.58%), and 1.12% for 5,000 lines; p95 time to build the input was 7.7ms and 34.4ms. Tests confirm that every range partly crossing a table, code block or math block is refused, and in 3 real HTTP requests a partial code-block selection was blocked with 422.

  5. Tracing LLM runs while bounding sensitive data

    Problem The evaluation loop ran as plain Python, so extraction, evaluation, correction and retry could not be inspected stage by stage. Turning tracing on, however, would send raw tool lookups and personal data to an external tracing tool, weakening security.

    Decision I separated what has to be observable from what must not leave the system. Graphs that decide answer quality get node-level tracing; the agent graph that edits a user’s own documents runs with tracing off and keeps raw content out of persisted state.

    Implementation Rebuilt the evaluation loop as a LangGraph graph so answer generation, evaluation and retry each surface as their own node. Even with tracing configured on, a missing key leaves graph execution intact and skips only tracing, which avoids warnings and pointless network retries. The agent graph disables tracing and excludes raw tool lookups from persisted state, and that state is deleted before the run record once retention ends. The prompt log I write myself masks numeric personal data before writing.

    Verification Ran real requests and confirmed that graph nodes and model call spans appear in the trace. Timing experiments run with tracing and prompt logging both disabled, so instrumentation overhead stays out of the measured times.

03 Improved processing flow

ARCHITECTUREMermaid diagram
flowchart TD
 U[User describes a recurring task] --> S[Draft Skill, mask personal data and check prompt injection]
 S --> P[User review and recheck on publication]
 P --> K[Store reusable instructions]
 Q[User requests document work] --> R[Router selects task and conditions]
 K -. Reuse when needed .-> R
 R --> E[Edit while preserving goal and scope]
 R --> F[Plan folder organization and compare with request]
 E --> V[Check format, edit scope, personal data and injection]
 V --> D[Show validated changes and diff]
 F --> D
 D --> A{User approval}
 A -->|Approve| W[Apply edits or organize folders]
 A -->|Cancel| N[Keep originals]

User-defined instructions and the document workflow that uses them. Executors do not reclassify the task; they validate proposed changes and apply them after user approval.

View Mermaid source
flowchart TD
 U[User describes a recurring task] --> S[Draft Skill, mask personal data and check prompt injection]
 S --> P[User review and recheck on publication]
 P --> K[Store reusable instructions]
 Q[User requests document work] --> R[Router selects task and conditions]
 K -. Reuse when needed .-> R
 R --> E[Edit while preserving goal and scope]
 R --> F[Plan folder organization and compare with request]
 E --> V[Check format, edit scope, personal data and injection]
 V --> D[Show validated changes and diff]
 F --> D
 D --> A{User approval}
 A -->|Approve| W[Apply edits or organize folders]
 A -->|Cancel| N[Keep originals]

04 Results and lessons

Reusable tasks and approval workflow; editing passes 82.5% → 91.2% (114 drafts)

Users can define recurring tasks, publish them as Skills and reuse them. The final instructions are rechecked on publication, and actual document changes require a preview and user approval. A plan–approve–execute check of folder organisation on 6 documents confirmed it worked correctly. Editing quality was compared on 114 drafts from 19 request types. Passing the edit goal down to the editing step and fixing the retry input raised passes from 94 to 98; evaluating request fulfillment and meaning preservation separately and allowing one rewrite raised them to 104. I separated the generation and evaluation prompts to stop the evaluator rejecting valid drafts, and no edit is returned if a draft fails again. AI drafts instructions and edits, code checks permissions and execution scope, and users decide publication and final application.

Original Agent94 / 11482.5%
Code changes, without evaluator98 / 11486.0%
Final implementation with evaluator104 / 11491.2%
Scope and tradeoffs

Normalized average editing-evaluation time increased from 11.11s to 18.92s. I accepted the latency but limited the number of rewrites.

Building user-defined Agent Skills (Korean) ↗The two sides of LLM evaluation for reliability (Korean) ↗
CASE 03Fruition / AI Agent · Compensating recovery · Audit history

Cancelling an AI task and undoing what it already saved

An AI task saves documents, folders, wiki pages and skills several times between model calls. Stopping execution alone leaves part of a cancelled task behind, so I built a path that undoes already-committed changes in reverse order. Contributions left by other tasks survive, because recovery excludes only the target contribution from an append-only history.

2026.08 — 2026.09

Compensating recoveryIdempotency keysAppend-only historyPostgreSQL execution lockAudit history

01 Background

An AI task runs across several stores and cannot be wrapped in one long transaction, so I had to decide what a cancellation should undo and how far it should reach.

What solving it enables

If cancelling leaves the AI's changes behind, user data gets damaged, users stop handing over large jobs, and churn and support requests follow. Reliable rollback is what lets users trust AI with large jobs.

02 Decisions and implementation

  1. Deciding that stopping execution was not enough

    Problem An AI task saves documents, folders, wiki pages and skills several times between model calls, so stopping execution left part of a cancelled task in place. Yet execution spanning the core DB, the AI DB and object storage could not be wrapped in one long transaction either.

    Decision I chose to undo already-committed changes too. Asking the model to perform the inverse cannot guarantee original values, approval scope or idempotency, and locking the whole service in one transaction cannot absorb long-running model, HTTP and object-storage work or its failures, so both were rejected.

    Implementation The AI side derives reverse actions from the run record while the backend performs atomic recovery of business data. Agent tools store a pre-change snapshot with reverse-action arguments and an idempotency key tied to the original approval, and ordinary tasks record before and after values through each database’s triggers inside the same transaction. Object storage persists the original and the intended result before writing.

    Verification A test that runs the real worker makes model creation fail during recovery and confirms the cancellation finishes without any model or gateway call. Cancelling a two-folder, six-document organisation task at each of 9 points returned the workspace to its original state, and after a lost response a new worker resumed recovery with the same idempotency key.

  2. Blocking later writes and undoing children first

    Problem A handler still running after a cancellation would overwrite recovered values, and undoing a parent first left the changes its children had made behind. Recovery could fail, leaving users unable to return to the version they wanted.

    Decision Persist the cancellation first to block later writes, and once the running handler finishes, undo children first in reverse order.

    Implementation The agent turn and recovery share one PostgreSQL execution lock. The lock releases when a worker connection ends, so a leftover in-progress record cannot block recovery. Body recovery is saved as a new revision, so a late event cannot overwrite the source again.

    Verification Tests confirm that writes after cancellation are refused, recovery waits for the running handler, and children are restored before their parent. Even after the running process was force-killed, recovery waited on the same lock and finished, and a late event from an earlier revision could not overwrite the restored body. If another user later changed the data, recovery did not overwrite it and stayed retryable; cancelling again after the conflict cleared completed normally.

  3. Recovering without erasing other tasks’ contributions

    Problem A wiki concept page merges contributions from several documents, so overwriting it with an older snapshot also removed valid contributions added later by other documents.

    Decision Removing just the relevant part from the finished result was hard because which sentence came from which document cannot be told apart reliably. Restoring an entire older snapshot was rejected too, because information is lost if a new document contributed in between. Instead the history is append-only and recovery excludes only the target contribution before reassembling.

    Implementation Operations, page revisions and per-document contributions are recorded append-only, and recovery restores or reassembles the page with the target contribution excluded. Pages and history are soft-deleted rather than removed, which preserves the audit trail.

    Verification Right before recovery, the contribution list (operation, revision, active flag) is re-read under a lock, and recovery is refused if it differs from the preview. Tests confirm that pages are rebuilt from the selected contributions only and that if any page fails, no current state changes. Recovery itself is recorded as a new operation and revision.

03 Improved processing flow

ARCHITECTUREMermaid diagram
flowchart TD
 A[User cancels] --> B[Persist cancellation first · block later writes]
 B --> C[Wait for the running handler to finish]
 C --> D[Derive reverse actions from the run record · no model call]
 D --> E[Undo children first, in reverse]
 E --> F{Current values and references unchanged}
 F -->|Yes| G[Atomic recovery · body saved as a new revision]
 F -->|No| H[Leave as rollback_failed for retry]
 G --> I{Full recovery confirmed}
 I -->|Yes| J[cancelled]
 I -->|No| H

Cancellation is persisted first to block later writes, then reverse actions derived from the run record undo children first. Cancellation is only final once full recovery is confirmed; otherwise it stays retryable.

View Mermaid source
flowchart TD
 A[User cancels] --> B[Persist cancellation first · block later writes]
 B --> C[Wait for the running handler to finish]
 C --> D[Derive reverse actions from the run record · no model call]
 D --> E[Undo children first, in reverse]
 E --> F{Current values and references unchanged}
 F -->|Yes| G[Atomic recovery · body saved as a new revision]
 F -->|No| H[Leave as rollback_failed for retry]
 G --> I{Full recovery confirmed}
 I -->|Yes| J[cancelled]
 I -->|No| H

04 Results and lessons

A cancelled task’s changes are undone while other tasks’ contributions survive

Content, names, parents and ordering that users see are restored, while version, audit and idempotency records are kept. Recovery runs from the run record alone, with no model settings or calls, and it checks current values and downstream references so it never overwrites another task’s work. Cancellation is final only once full recovery is confirmed; conflicts and unclear outcomes stay retryable. For a feature where AI edits a user’s documents, I settled what could be undone and what had to be recorded before building it. In a manual run on real Gemini, Kafka, PostgreSQL and Redis, nothing changed before approval, and cancelling during the last document move restored 2 folders and 6 documents with 8 inverse operations.

Stores covered by recovery3core DB · AI DB · object storage
Recovery executionNo model callsReverse actions from the run record only
Cancellation becomes finalOn confirmed recoveryOtherwise kept retryable
Scope and tradeoffs

This case is about design and unit-level verification; I did not measure a recovery success rate under production traffic. Preserving per-source contributions from ingest onward costs storage. Without a snapshot of the remaining combination a page has to be reassembled, and older tasks carried no snapshot, so runs and queues had to be drained before deploying.

CASE 04Fruition / PDF conversion · Data pipeline

PDF → Markdown: internal pass rate 45.17% → 89.89%

To use research papers as knowledge, PDF-to-Markdown conversion had to preserve table values and equations. I traced omissions introduced during AI repair and split processing paths by content type and original layout.

2026.07 — 2026.09

Open-source PDF conversion: AnyDoc, DoclingLLM APIsLLM (Qwen2.5 7B)PythonLLM-as-a-Judge

01 Background

Using research papers as service knowledge required Markdown that preserved not only body text but also tables, equations and figures.

What solving it enables

If PDF restoration drops and key figures go missing, documents cannot fully enter the Wiki, users get a lower-quality service, and they may leave. Restoration quality is the foundation of Wiki and answer quality.

02 Experiments and improvements

  1. Selective repair and missed errors

    Problem Open-source conversion broke table row/column relationships and equations, so the key figures in PDF papers came out in a form hard to bring into the Wiki. I needed a way to repair the damaged parts.

    Experiment and result Docling extracted the structure, and only damaged tables and equations were repaired with local Qwen sLLMs. Tables, where row/column positions matter, were handled mainly by the vision model (qwen2.5vl:7b); equations were drafted by the text model (qwen2.5:7b) and checked by the vision model only when needed. This restored more than applying both models to everything and cut sLLM processing time by about 42%. Keeping OCR and prompts unchanged and only re-cropping images with Docling's strict bboxes fixed two previously failing equations. Evaluating repaired and untouched regions separately, 85 of the 96 sLLM-repaired regions passed, but 107 errors or omissions remained in untouched content. A separate experiment adding OCR comparison raised the pass rate from 80.00% to 97.53%, but also selected 50 correct regions for repair; the sum of processing-stage times was about 12 minutes 35 seconds.

    Decision Selecting repair targets missed errors, and extra checks to catch them added cost. I decided to test whether having the vision sLLM read every original page could avoid this dependence on damage detection.

  2. Table/equation errors and cost in full-page sLLM conversion

    Problem Letting AI decide what to fix missed errors, and adding OCR checks also flagged correct content, increasing check time and cost. I needed a way to pick and fix only the damaged parts.

    Experiment and result Full conversion of 25 research PDFs with the vision sLLM (qwen2.5vl:7b), which can check the original page directly, (415 pages) achieved a 91.92% content-preservation pass rate. However, only 58.84% of tables and equations fully matched in values and structure, and processing every page imposed a heavy processing load.

    Decision I judged that sending all content through the model the same way was the problem. Body text should use AI only where needed, and tables and equations should get inputs that preserve their structure. So I split the paths for body, tables and equations, and looked for fast body extraction with AI fixes only where needed.

  3. Comparing text extraction, adopting AnyDoc, and moving to commercial APIs

    Problem Full-page sLLM processing still left table/equation errors, and sending all body text to the model was a heavy load. To cut that load and improve speed and accuracy, I needed separate repair for the tables and equations that usually break, alongside fast body extraction.

    Experiment and result I found AnyDoc on Threads and, after seeing its GitHub comparison with Docling, tested it on the same 4 PDFs (30 pages). The recorded Docling run took 134.19s, while AnyDoc text extraction took 1.68s, about 80 times shorter. Docling’s time also included OCR and table analysis. One AnyDoc document had garbled characters because the PDF lacked mappings from character codes to actual characters.

    Decision The processing scopes differed, so this was not an 80-fold improvement in complete conversion. Still, AnyDoc was useful for fast text extraction. I kept separate table/equation repair, switched body extraction to AnyDoc, used AI on garbled or misordered pages, and preserved original figures. With no early users yet, running GPU servers for an sLLM would cost more than it saved, so from this point I switched the repair model to commercial LLM APIs.

03 Improved processing flow

ARCHITECTUREMermaid diagram
flowchart TD
 A[Original PDF] --> B[Separate text, tables, equations and figures]
 B --> C[Preserve text context and position]
 B --> D[AI reads original table and equation regions]
 B --> E[Preserve original figures]
 C --> F[Reassemble in original order]
 D --> F
 E --> F
 F --> G{Check markers and missing content}
 G -->|Pass| H[Converted document]
 G -->|Mismatch| I[Keep original content for that region]

A conceptual view of processing by content type and reassembling at the original positions. This is the current design; experiments did not all use the same path.

View Mermaid source
flowchart TD
 A[Original PDF] --> B[Separate text, tables, equations and figures]
 B --> C[Preserve text context and position]
 B --> D[AI reads original table and equation regions]
 B --> E[Preserve original figures]
 C --> F[Reassemble in original order]
 D --> F
 E --> F
 F --> G{Check markers and missing content}
 G -->|Pass| H[Converted document]
 G -->|Mismatch| I[Keep original content for that region]

04 Results and lessons

Pass rate against 30 original pages: 45.17% → 89.89%

Split the same papers into 445 evaluation units covering body text, tables, equations and other content, then compared them with the originals. The existing Docling output and improved pipeline output were evaluated with method names hidden. The practical pass rate rose from 201 (45.17%) to 400 (89.89%), and the strict rate, which requires values and symbols to match exactly, rose from 193 (43.37%) to 393 (88.31%).

Existing Docling45.17%30 pages total
Improved pipeline89.89%30 pages total
Scope and tradeoffs

Internal LLM evaluation of 4 PDFs (30 pages). Minor differences were allowed for text, headings and figures; tables and equations had to match in values, symbols and structure.

Trying a small LLM for PDF → Markdown (Korean) ↗Trying the open-source PDF converter AnyDoc (Korean) ↗
CASE 05Fruition / Kafka asynchronous processing · AI pipeline

Parallel document analysis: 282.11s → 73.89s

Sequential Wiki generation could make large documents slow to process and hurt the user experience. I measured worker scaling and resource use, then set different scaling policies for document analysis and embedding generation.

2026.09

AI pipelineKafkaPostgreSQLConcurrency and memory

01 Background

Sequential processing left later documents waiting on earlier external AI responses. Kafka distributes per-document jobs (re-readable, ordered per key), so long AI work runs asynchronously while IDs prevent duplicates. Raising workers from 1 to 4 cut analysis of 4 documents from 282.11s to 73.89s, but raising concurrent requests per worker from 2 to 4 slowed it to 95.92s, and 2 embedding processes on one GPU each went from about 9.5s to 19–21s. The two stages scale differently, so I set separate thresholds.

What solving it enables

If Wiki generation is slow, users cannot use what they uploaded and may leave before seeing any value. Shorter waits let them search and ask about documents right after upload.

02 Experiments and improvements

    03 Improved processing flow

    ARCHITECTUREMermaid diagram
    flowchart TD
     A[Document processing request] --> K[Kafka distributes document jobs]
     K --> W[Multiple workers with per-worker request limits]
     W --> L[Lock writes within the same workspace]
     V[Embedding generation] --> G[Check contention on a shared GPU]
     L --> D[Check time, memory and content preservation]
     G --> D
     D --> R[Choose scaling policy per stage]

    Evaluated scaling separately for Wiki generation and embedding generation.

    View Mermaid source
    flowchart TD
     A[Document processing request] --> K[Kafka distributes document jobs]
     K --> W[Multiple workers with per-worker request limits]
     W --> L[Lock writes within the same workspace]
     V[Embedding generation] --> G[Check contention on a shared GPU]
     L --> D[Check time, memory and content preservation]
     G --> D
     D --> R[Choose scaling policy per stage]

    04 Results and lessons

    73.8% less analysis time for 4 documents · 300 queued jobs with zero loss

    With 4 workers and 2 concurrent requests each, total processing time for 4 documents fell by about 73.8%. Combined process memory rose from 351.7MiB to 1,401.0MiB, about fourfold. Of 16 facts fixed in advance, 10, 14 and 11 were fully preserved with 1, 2 and 4 workers, so although each condition ran once on a small sample, I did not treat quality as identical. I excluded raising concurrent requests further and parallelising embedding generation on the same GPU, since both made things slower. External-response waits and device contention are different bottlenecks, so each stage needed its own scaling approach. Replaying saved AI responses for 100 documents under 3 configurations processed all 300 jobs without omissions or failures. Judging that the memory increase could be absorbed by distributing AI functions on EKS as MSA, I used at most 4 workers for parallel document processing.

    1 worker282.11s4 documents, 8 calls
    4 workers73.89sUp to 8 concurrent API requests
    Combined process memoryAbout 4×351.7 → 1,401.0MiB
    Scope and tradeoffs

    One analysis run per configuration on the same documents. These are not end-to-end timings including storage and post-processing.

    CASE 06Fruition / Coding agent operations · MSA migration

    Coding agent loop: 16 MSA migration PRs merged in 5 days

    I delegated moving the AI features into the MSA structure to coding agents, while I set the work plan and review criteria and decided what to merge. Agents ran in parallel in a separate workspace per branch, and a coordinator agent managed order and priority.

    2026.08

    Coding agentsClaude CodeMSACode review

    01 Background

    The MSA migration had to be done within a week so we could deploy on schedule and get feedback on the new structure. The features were already built, so moving them meant too many changes to make by hand in time, and the work was split across several streams. I built a coding-agent loop so the work could keep going while I rested or slept. With about a week left before the MSA migration deadline, moving the AI features into the new service structure had split into several streams. Long single sessions lost track of the plan as context was summarized.

    What solving it enables

    A late migration would push back both deployment and feedback, and fixing code that drifted from the plan adds rework cost. The goal was to hold the schedule with the same team.

    02 Loop design and decisions

    1. Building an agent loop with separate roles

      Problem Giving one session all the work blurred the plan and criteria as context was summarized. Agents then drifted from the plan, leaving commits missing or producing code that differed from it. Fixing that cost more time, so I set out to cut that waste.

      Experiment and result Fixed the work plan and criteria in documents and split roles into implementation, review and integration review. Each branch had its own workspace with two agents running in parallel, and a coordinator agent judged dependencies and priority between tasks.

      Decision Sessions could change and still continue from the same documents, so splitting long work across agents did not scatter its direction.

    2. PR timeline: 8/9 → 8/13

      Problem Tasks depended on each other, so opening everything at once risked conflicts and rework.

      Experiment and result Opened #156–#159 on 8/9 and merged them on 8/10, ran #167, #169, #170 and others in parallel on 8/11, then fixed integration defects in #175 on 8/13. 16 PRs merged over 5 days.

      Decision Opening the next batch only after prerequisite work merged kept conflicts down while still running in parallel.

    3. Pre-merge review and merge decisions

      Problem Merging agent-made changes as-is risked bringing defects in with them.

      Experiment and result Merged only after the 13 required fixes from review were applied and regressions checked. For example, a defect where restoring a document returned its previous body was fixed before merge.

      Decision Instead of writing the code myself, I made my role defining the pass criteria and deciding merges by them.

    03 Improved processing flow

    A dev-msa coordinator agent with branch workspaces (PRs #167, #169, #170), each running two agentsView original size ↗
    Agents working in parallel in branch workspaces under a coordinator agent (2026.08.11).
    ARCHITECTUREMermaid diagram
    flowchart TD
     P[Work plan and criteria docs · me] --> C[Coordinator agent · dependencies and priority]
     C --> W1[Branch workspace · implementer + reviewer agents]
     C --> W2[Other branch workspaces · in parallel]
     W1 --> R[PR creation · integration review]
     W2 --> R
     R --> F[Required fixes applied · regression check]
     F --> J[Merge decision · me]

    I set the plan and criteria; agents with separate implementer and reviewer roles worked in parallel per branch.

    View Mermaid source
    flowchart TD
     P[Work plan and criteria docs · me] --> C[Coordinator agent · dependencies and priority]
     C --> W1[Branch workspace · implementer + reviewer agents]
     C --> W2[Other branch workspaces · in parallel]
     W1 --> R[PR creation · integration review]
     W2 --> R
     R --> F[Required fixes applied · regression check]
     F --> J[Merge decision · me]

    04 Results and lessons

    16 PRs merged in 5 days · 13 required fixes applied before merge

    Merged 16 PRs over 5 days, from August 9 to 13. The 13 required fixes raised in pre-merge review were applied and checked for regressions. One defect the review caught: restoring a document returned its previous body. The coding agents did the implementation; I designed the loop, wrote the work plan and criteria documents, reviewed results and made merge decisions.

    PRs merged162026.08.09 – 08.13
    Required fixes before merge13From review
    Agents per branch2Implement · review
    Scope and tradeoffs

    I judged merges by criteria documents and review results rather than writing the code myself, so defects outside the review criteria can slip through. Refining the criteria documents remains ongoing work.

    PROJECT OVERVIEW

    Pilltip

    A personalized AI medication service that explains drug information in plain language and helps users manage their own and their family’s medications and schedules.

    How it started

    Started as a Pusan National University capstone project. Drug information is scattered, so it is hard to check whether a given medicine is safe for your current health condition. We set out to make that easy.

    Team and my role

    MY ROLEBackend & AI

    Drug and supplement DB, search and autocomplete, reminders, risk and caution information, data preparation, AI chatbot

    • Backend and AI developmentImplemented drug-related backend features, reminders and logs, friend invitations, family profile switching, the data preparation pipeline and AI chatbot.
    • Drug and supplement modeling, search, autocomplete and risk informationDesigned the drug and health-supplement DB and implemented search autocomplete with Elasticsearch and Redis, plus Drug Utilization Review (DUR) risk and caution information. Cut search responses that took 5–7s because of LIKE queries and multiple JOINs to 0.8s on average.
    • Medication reminders and logsImplemented reminders and medication logs using FCM.
    • Friends and familyImplemented deep-link friend invitations and family profile switching.
    • Data pipeline: about $200 estimated, $11.18 actually spentBuilt a data preparation pipeline to rewrite all drug information covered by the service into friendly, everyday language.
    • RAG-based AI chatbotBuilt an AI-orchestrated chatbot that retrieves relevant information and connects search and DUR checks according to each question.
    • Extended support to health functional foods after exhibition feedbackThe first service covered only drugs, and most first-exhibition feedback asked to check health supplements too. Within the supplement data obtained from public datasets, I extended search and autocomplete, caution and interaction info, medication reminders and logs, and chatbot answers, turning the requests into shipped features.
    Android, 1
    The Kotlin-based Android app
    Backend & security, 1
    User DB, login, questionnaires, AES-GCM encryption
    Design
    Designer
    GitHub repository ↗

    Features and service screens

    CASE 07Pilltip / Data pipeline · GPT Batch API · PySpark

    Drug data pipeline: $200 estimated → $11.18 spent

    Built a data preparation pipeline to rewrite difficult drug information in plain language. Split the text into sentence blocks, converted each unique sentence once and mapped results back to each drug, for $11.18 in actual API spending. In 2026 I reproduced the same preparation in PySpark to measure the effect of each stage.

    2025.07 · reproduced 2026.10

    Data pipelineGPT Batch APIPySparkMinHashLSH

    01 Background

    Rewriting about 44,000 drug records (about 1GB) as-is was estimated to cost about $200 in API calls. I needed a way to explain every drug within budget without cutting the number of drugs covered.

    What solving it enables

    If conversion costs exceed the budget, fewer drugs get explanations and the service shrinks. Lowering per-item cost lets the same budget cover every drug.

    02 Problem analysis and implementation

    1. Removing exact duplicate sentences

      Problem Converting about 44,000 records totaling 1GB was estimated to cost about $200 in API usage. I needed to cover all drugs within budget.

      Experiment and result Examined 100 records and found about 70% duplicate content. Used this sample and a work plan to secure 7 days for preprocessing. Split text into sentence blocks, merged exact matches and preserved links from each drug to its blocks.

      Decision Convert repeated sentences once and reuse them rather than cover fewer drugs.

    2. GPT Batch API conversion and mapping results back to drugs

      Problem Deduplication reduced input, but I still needed to minimize model costs.

      Experiment and result The work did not require immediate responses during service use, so batch waiting was acceptable. Converted unique sentences with GPT Batch API, then reused the preserved drug-to-block mappings to attach results to each drug. Actual API spending was $11.18 versus about $200 estimated for all original text.

      Decision Combined deduplication and batch conversion to deliver the feature without reducing drug coverage.

    3. 2026 PySpark reproduction: measuring ending normalization and similar-sentence merging

      Problem Part of the 2025 code and the raw files no longer existed, so the effect of stages after exact deduplication could not be shown in numbers.

      Experiment and result Re-collected 42,714 products from the MFDS public API, split them into 3,299,602 blocks following the XML structure, and reproduced normalization and exact deduplication in PySpark. Then applied an ending-normalization dictionary and MinHashLSH (character 3-grams, Jaccard distance), comparing reduction and merged samples at thresholds 0.1, 0.2 and 0.3. PySpark split about 3.24 million blocks across cores and can scale to a cluster as data grows.

      Decision At 0.3, text shrank 18.78% but sentences about different conditions (Lapp vs. total lactase deficiency) merged, and at 0.2 some precaution sentences were lost. Wrong merges are costly for medical information, so I chose 0.1 (a further 6.69%) with guards that merge only when numbers and applicability conditions match.

    03 Improved processing flow

    Output showing drug text split into indexed blocks with drug ID to block ID mappingsView original size ↗
    Processing output preserving unique sentence blocks and their links to each drug.
    ARCHITECTUREMermaid diagram
    flowchart TD
     A[Original drug text: about 1GB] --> B[Split into sentence blocks]
     B --> M[Keep drug ID to block ID mappings]
     B --> C[Merge exact duplicate sentences]
     C --> G[Convert unique sentences with GPT Batch API]
     G --> R[Map converted text back to each drug]
     M --> R
     C -.-> S[2026 PySpark reproduction · measured ending normalization and MinHashLSH]

    Converted each repeated sentence once and kept per-drug mappings to attach results. In 2026 the same process was reproduced in PySpark and measured.

    View Mermaid source
    flowchart TD
     A[Original drug text: about 1GB] --> B[Split into sentence blocks]
     B --> M[Keep drug ID to block ID mappings]
     B --> C[Merge exact duplicate sentences]
     C --> G[Convert unique sentences with GPT Batch API]
     G --> R[Map converted text back to each drug]
     M --> R
     C -.-> S[2026 PySpark reproduction · measured ending normalization and MinHashLSH]

    04 Results and lessons

    Actual API spend $11.18 · PySpark reproduction cut text by 94.1%

    Completed conversion for $11.18 in actual API spending versus about $200 estimated for all original text. Reusing converted unique sentences kept the feature within budget without reducing the set of drugs covered. Part of the 2025 preparation code no longer existed, so in 2026 I re-collected 42,714 products (960MB of plain text) from the MFDS public API and reproduced the same preparation in PySpark. 3,236,626 blocks reduced to 176,648 unique blocks, cutting text from 744MB to 44.1MB (−94.1%). An ending-normalization dictionary then removed 0.78%, and MinHashLSH similar-sentence merging a further 6.69%.

    Estimated cost for original textAbout $200Estimate
    Actual API spending$11.182025 conversion
    Exact deduplication−94.1%2026 PySpark reproduction · 744MB → 44.1MB
    Scope and tradeoffs

    About $200 was the upfront estimate; $11.18 was actual spending. The 2026 figures come from re-collected data, not the 2025 dataset. The reproduction code was written with AI coding tools; I designed the experiment and chose the thresholds.

    CASE 08Pilltip / AI Agent · Sensitive data boundaries

    Medication chatbot: keeping health profiles out of external AI inputs

    Built a medication chatbot using RAG and AI orchestration for app integration. Separated symptom-based drug discovery from health-profile decisions, using the model for language understanding and code to control sensitive profile use.

    2025.09

    Spring AIWeaviateRAGAI Orchestration

    01 Background

    Users describe symptoms in different words from drug indications, so keyword search could not match symptoms to drugs. Personalization also needed pregnancy status, current medications and underlying conditions, but sending that health profile to an external LLM as-is would make it hard to control where sensitive data goes.

    What solving it enables

    Sending health data to external AI brings leak risk and regulatory burden, while not using it loses personalization, the core feature. Deciding internally keeps both.

    02 Design and implementation

    1. Semantic search connecting symptoms to drug indications

      Problem Everyday symptom descriptions and drug indications use different words for the same meaning. Related information needed to be found without exact keyword matches.

      Decision I first started building a symptom synonym dictionary and wrote code for it. But there were too many symptoms, and when users described several at once the dictionary entries overlapped. I still had to pick the drugs that fit that person, yet vague drug descriptions matched almost every symptom. So I used semantic search to link natural-language symptoms to drug indications, and split processing paths by what each question needed.

      Implementation Among free vector databases with hybrid search I tried Milvus first, but it was too heavy, so I stored drug indications in the lighter Weaviate. Because the backend was Spring, I ran orchestration with Spring AI on the same server instead of adding a separate Python container. Stored drug indications in Weaviate for semantic symptom search. Built paths for drug discovery, medication risks and dosage information according to the question.

      Verification In user-scenario E2E tests in August 2025, natural-language symptoms were routed to the right path (drug search, medication risk, dosage). I did not keep a numeric accuracy evaluation per symptom.

    2. Internal health-profile processing, separate from external AI inputs

      Problem Personalization required pregnancy status, medications and underlying conditions, but sending those profiles to an external LLM risked exposing sensitive information.

      Decision At first I considered sending the health profile to the external AI with sensitive content filtered out. But there were too many expressions and cases to filter, and I judged that code could not cover them all. So I separated understanding symptom language from deciding profile conditions, and kept profile decisions in internal code where inputs and rules can be controlled directly.

      Implementation Excluded pregnancy, medication and underlying-condition fields from external LLM inputs. Connected internal profile decisions to symptom search results to provide personalized information.

      Verification Pregnancy status, current medications and underlying conditions are never put into external LLM inputs; an internal tool uses them to judge contraindications, duplicates and interactions and filter out unsuitable drugs. The AI guidance on the drug detail screen did not send these three fields either.

    03 Improved processing flow

    ARCHITECTUREMermaid diagram
    flowchart TD
     S[Natural-language symptoms] --> V[Weaviate: semantic search of indications]
     P[Pregnancy, medications and underlying conditions] --> T[Internal tool: profile decisions]
     V --> R[Personalized information]
     T --> R
     B[External LLM input boundary] -.-> X[Exclude these profile fields]
     classDef accent fill:#e5edff,stroke:#4164c8,color:#142b60;
     class T,B,X accent;

    A conceptual view of responsibilities and data boundaries, not the exact call sequence or a guarantee against every form of data leakage.

    View Mermaid source
    flowchart TD
     S[Natural-language symptoms] --> V[Weaviate: semantic search of indications]
     P[Pregnancy, medications and underlying conditions] --> T[Internal tool: profile decisions]
     V --> R[Personalized information]
     T --> R
     B[External LLM input boundary] -.-> X[Exclude these profile fields]
     classDef accent fill:#e5edff,stroke:#4164c8,color:#142b60;
     class T,B,X accent;

    04 Results and lessons

    Separating symptom search from internal health-profile decisions

    Enabled natural-language drug discovery while excluding pregnancy status, current medications and underlying conditions from external LLM inputs. Explicitly separated information sent to AI from information evaluated by internal code. In a domain that handles sensitive data, I settled the boundary between what the model may judge and what code stays responsible for before writing the implementation.

    Scope and tradeoffs

    Profile-rule changes require code updates. Even so, limiting how much sensitive health data reaches external models came first, so this decision stays in code rather than with the AI. It removes specific profile fields from external inputs; it does not measure blocking across every possible path. With more users, AI processing should move to a separate service.

    BEYOND THE PROJECTS

    Beyond project development

    OPEN SOURCE ↗

    Rhwp · Fixing HWPX save errors

    Gave Claude Code a work plan and contribution rules to automate the flow from issue analysis to PR; 7 PRs merged, including the textFlow preservation fix (#1213), and listed as a contributor.

    GitHub repository ↗

    APPTIVE · Backend mentoring

    APPTIVE Backend Mentor Merit AwardEnlarge ↗

    Used my experience as a mentee to improve teaching. Led 6 mentoring and code-review sessions for 12 mentees.

    Activities ↗

    945-day Baekjoon problem-solving streak

    Baekjoon solved.ac streak calendars for 2022 to 2025Enlarge ↗

    Aiming for one problem a day, I reached a 945-day streak on solved.ac. 1,659 problems solved, solved.ac Platinum IV.

    Solutions repository ↗
    ENGINEERING JOURNAL

    Project reflections

    More blog posts ↗