본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 05. 17. 06:23

【로컬 LLM】 Qwen2.5-Coder:14B를 이용한 계산 화학 워크플로의 JSON화 검증

요약

본 기사는 로컬 LLM(qwen2.5-coder:14b)을 계산 화학 워크플로를 제어하는 구조화된 JSON 생성기로 활용할 수 있는지 검증한 내용을 다룹니다. LLM이 복잡한 화학적 추론보다는 YAML/JSON 설정 해석, 작업 흐름 정의, 결과 정형화 등 '계산 화학의 잡무'를 담당하는 인터페이스 층으로 사용될 가능성을 제시합니다.

핵심 포인트

  • LLM을 계산 화학 엔진 대신 구조화된 JSON 생성기로 활용하여 워크플로 자동화를 시도한다.
  • qwen2.5-coder:14b는 Coder/Instruct 계열 모델로서, 엄격한 포맷이 요구되는 작업에 적합하며 VRAM 효율성 측면에서 현실적인 선택이다.
  • LLM은 화학 도메인 지식 기반의 최종 판단보다는 설정 독해 및 워크플로 JSON 생성 등 저계층 '잡무' 담당 역할에 국한하는 것이 바람직하다.
  • 이러한 접근 방식은 계산 화학 자동화 AI 에이전트의 인터페이스 층으로서 로컬 LLM 활용 가능성을 열어준다.

본고에서는 로컬 LLM을 단순한 채팅 상대가 아니라, 계산 화학 (Computational Chemistry) 워크플로를 제어하기 위한 JSON 생성기로 사용할 수 있을지를 검증하였다. 구체적으로는 Ollama 상에서 qwen2.5-coder:14b를 구동하여, 다음의 관점에 대해 평가하였다.

  • 화학 및 계산 화학에 관한 기본적인 추론이 가능한가
  • YAML/JSON 형식의 프로젝트 설정을 올바르게 읽을 수 있는가
  • 지정된 JSON Schema에 따라, 기계 판독 가능한 (Machine-readable) JSON만을 반환할 수 있는가
  • 생성된 JSON을 다른 모듈에 전달하여 실행할 수 있는가
  • RDKit, xtb, ASE, DFT, MLIP 등의 MI·계산 화학 워크플로에서 「출력 정형기 (Output Formatter)」로서 사용할 수 있는가

이 기사에서는 도입 절차, 검증용 스크립트, 테스트 프롬프트, 평가 관점, 결과 정리 방법을 정리한다.

계산 화학이나 재료 정보학 (Materials Informatics, MI)에서는 실제 계산이나 구조 생성 자체는 RDKit, xTB, ASE, Gaussian, ORCA, VASP, fairchem, MACE 등의 기존 도구에 맡기는 경우가 많다.

한편, 이러한 도구들을 연결하는 워크플로에서는 다음과 같은 작업이 빈번하게 발생한다.

  • SMILES나 XYZ 파일의 리스트화
  • 계산 조건의 보완
  • 전하·스핀 다중도·계산 방법의 지정
  • 올바른 순서로의 계산 실행 및 결과 처리
  • 출력 파일명의 정형화
  • 입력 YAML의 해석
  • 결과 집계용 JSON/CSV 생성
  • 에러 발생 시의 재시도 조건 결정

... etc.

이러한 것들은 완전히 결정론적인 (Deterministic) 처리만으로는 다소 유연성이 부족하다. 반면, LLM에 모든 것을 맡기면 설명문이나 Markdown 표기법 등이 혼입되어, 후속 모듈이 읽을 수 없는 출력이 되어버리는 경우가 있다.

따라서 본고에서는 로컬 LLM을 「화학 계산 엔진」이 아니라, 계산 화학 워크플로용 구조화 JSON 생성기로서 평가한다. 이 방침이 유망하다면, 계산 화학 워크플로를 자동화하는 「계산 화학 AI 에이전트」의 인터페이스 층으로서 로컬 LLM을 활용할 수 있는 가능성이 있다.

이번에 LLM으로 qwen2.5-coder:14b를 선택한 이유는 다음과 같다.

  • Coder/Instruct 계열의 모델이며, JSON, Python, CLI, 설정 파일을 다룰 때의 견고함이 뛰어나다
  • 파라미터 사이즈가 14B 규모로 적당하며, 16GB VRAM급 GPU에서의 동작이 현실적이다
  • 추론 (Reasoning) 특화 모델과 달리, 불필요한 사고 과정 (Thinking process)을 출력하기 어렵다
  • Ollama에서 간단히 pull 할 수 있다
  • 구조화된 출력 (Structured output)의 검증 대상으로 다루기 쉽다

첫 번째 이유는 중요하다. reasoning/thinking 계열의 모델은 추론 트레이스 (Reasoning trace)를 표준 출력으로 내뱉기 때문에 (섹션이 출력됨), 구조화된 출력이 요구되는 경우에는 사용하기 어렵다. 엄격한 입력 포맷이 요구되는 이번과 같은 케이스에서는 Coder/Instruct 모델을 이용하는 것이 합리적이다.

두 번째 이유는 GPU의 VRAM (메모리) 용량에 관한 것이다. 현실적인 제약으로서, 16 GB 정도의 GPU (5070Ti나 5080 등)에 올려서 구동할 수 있는 모델의 파라미터 수는 한정적이다. 예를 들어 최근에는 qwen3-coder 시리즈가 공개되어 있으나, 2026년 5월 현재 최소 모델은 30b 파라미터 (Q4_K_M) 모델이며, 이는 VRAM이 32 GB인 RTX5090 클래스가 아니라면 애초에 모델을 불러올 수 없다. qwen2.5-coder:14b-instruct는 속도·안정성·요구 VRAM의 밸런스가 좋기 때문에 선택하였다.

한편, Qwen2.5-Coder는 화학 특화 모델이 아니다. 따라서 본 기사에서는 LLM에게 화학적인 최종 판단을 맡기는 것이 아니라, 다음과 같이 역할을 나눈다.

(저계층의) LLM에게 맡기는 것의 예:
- YAML/JSON 설정의 독해
- workflow JSON의 생성
...

화학 도메인 지식을 필요로 하는 추론에는, 더 높은 계층에 추론 전용 모델을 배치하여 운용하는 것이 좋다. 여기서는 Qwen2.5-Coder가 저계층의 「계산 화학의 잡무 담당자」로서 기능하기를 기대하고 있다.

Qwen2.5는 다소 오래된 (그렇다 해도 약 1년 전) 소규모 모델이지만, JSON 생성기 용도로 굳이 최신 거대 모델을 사용할 필요성은 없다.

OS:
Windows 11 + WSL2 Ubuntu 24.04.1
CPU:
...
curl -fsSL https://ollama.com/install.sh | sh
>>> Cleaning up old version at /usr/local/lib/ollama
[sudo] password for hogehoge:
>>> Installing ollama to /usr/local
...
$ ollama --version
ollama version is 0.24.0

파일 크기는 9 GB 정도.

ollama pull qwen2.5-coder:14b
pulling manifest
pulling ac9bc7a69dab: 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 9.0 GB
pulling 66b9ea09bd5b: 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 68 B
...

모델 목록을 확인한다.

$ ollama list
NAME ID SIZE MODIFIED
qwen2.5-coder:14b 9ec8897f747e 9.0 GB About an hour ago

Ollama 서버가 기동 중이며 모델 로드에 성공했는지 확인한다.

$ curl http://localhost:11434/api/tags
{"models":[{"name":"qwen2.5-coder:14b","model":"qwen2.5-coder:14b","modified_at":"2026-05-16T20:21:56.526874245+09:00","size":8988124298,"digest":"9ec8897f747e246e970bc5cfdda85d22f1123dc2e3d34978a010a75968716849","details":{"parent_model":"","format":"gguf","family":"qwen2","families":["qwen2"],"parameter_size":"14.8B","quantization_level":"Q4_K_M"}}]
$ ollama run qwen2.5-coder:14b
>>> Return only JSON. Create a JSON object with keys "status" and "message".
```json
{
"status": "success",
"message": "Request processed successfully."
}

JSON 형식으로서 올바르며, 이는 기대되는 동작이다.

Python에서 Ollama를 호출하기 위해 필요한 라이브러리를 도입한다. `.venv`가 홈 디렉토리에 존재하면 번거로우므로, `ollama`와 같은 적당한 디렉토리를 생성하여 그 안에서 검증하는 것이 좋다.

mkdir ~/ollama
cd ~/ollama
python -m venv .venv
...

$ pip freeze | grep -E "ollama|pydantic|PyYAML"
ollama==0.6.2
pydantic==2.13.4
...


이후, `source .venv/bin/activate`를 실행한 상태에서 검증을 실시한다.

from ollama import chat
response = chat(
model="qwen2.5-coder:14b",
...

$ python eval1.py
{
"formula": "CH4",
...


✅ 기대되는 동작이다.

다음으로, JSON Schema를 지정하여 출력 형식을 제약한다.

import json
from ollama import chat
from pydantic import BaseModel, Field, ValidationError
...

$ python eval2.py
{
"name": "Ethanol",
...


✅ 기대되는 동작을 보여주고 있다.

첫 번째는 LLM의 직접 출력이고, 두 번째는 Pydantic 검증 후의 정규화된 출력이다. 이 정도 수준의 구조라면 LLM이 직접 출력하더라도 구조가 무너지지는 않는 것으로 보인다.

또한, 분자식으로서 엄격한 출력이 요구된다면 `C2H6O` 방식이 더 나을 가능성도 있다.

다음으로, 계산 화학 워크플로 (Workflow)를 나타내는 JSON을 생성하게 한다. 상정하는 처리는 다음과 같다.

SMILES CSV

RDKit으로 3D 구조 생성
...


먼저, 다음과 같은 CSV 파일을 `~/ollama/examples/`에 배치한다. (`examples`라는 이름의 디렉토리는 사전에 생성해 둔다)

id,name,smiles,charge,spin_multiplicity,category,notes
mol_001,methane,C,0,1,closed_shell,"simple neutral closed-shell molecule"
mol_002,ethanol,CCO,0,1,closed_shell,"common organic molecule"
...


그 후, 다음 스크립트를 실행한다.

import csv
import json
from pathlib import Path
...

$ python eval3.py
{
"workflow_id": "smiles_to_xtb_relax",
...


JSON Schema (스크립트의 `class WorkflowInput` 부분)에는 `type`, `path`, `id_column`, `smiles_column`, `charge_column`, `spin_multiplicity_column`만을 규정하고 있으므로, CSV 열 정보의 일부(`name`, `category`, `notes`)는 "input"에 유지되지 않는다. 이는 정상적인 동작이다.

JSON Schema의 기재 내용은 LLM의 해석에 영향을 미치기 때문에, **순서를 포함하여** 신중하게 선택할 필요가 있다.

예를 들어, JSON Schema에 다음과 같이 `name`, `path`, `category`를 추가하면 "input"의 속성에도 반영된다. 이 값들은 CSV의 내용에 대응한다.

class WorkflowInput(BaseModel):
type: str
path: str
...

"input": {
"type": "smiles_csv",
"path": "examples/smiles_list.csv",
...


그렇다면, 추가 항목의 순서를 바꾸면 어떻게 될까?

class WorkflowInput(BaseModel):
type: str
path: str
...

"input": {
"type": "smiles_csv",
"path": "examples/smiles_list.csv",
...


이 경우에는 LLM이 독자적으로 `smiles_list.csv`의 내용으로부터 기재해야 할 사항을 판단하여 추가하고 있으며, 그 값은 CSV에 대응하지 않는다. 이처럼 JSON Schema의 기재 내용 및 순서는 LLM의 해석에 영향을 줄 우려가 있다.

이렇게 되는 이유로는, `type`과 `path`가 "파일 그 자체의 속성"이기 때문에, LLM의 내부에서는 이 직후의 항목도 "파일 그 자체의 속성"으로 인식해 버리고 있을 가능성이 생각된다.

CSV 파일의 열 정보를 올바르게 취득하려면, 다음과 같이 `_column` 라벨을 붙여 열 정보임을 명시해야 한다.

class WorkflowInput(BaseModel):
type: str
path: str
...

"input": {
"type": "smiles_csv",
"path": "examples/smiles_list.csv",
...


`notes_column`과 같이 라벨링을 해두면, 예를 들어 `notes` 부분이 `tags`나 `labels`로 되어 있더라도 `"notes_column": "labels"`와 같이 할당될 수 있다.

그렇기는 하지만, 유사한 이름이나 의미를 가진 열이 여러 개 있는 경우에는 LLM이 올바르게 추측하지 못할 가능성이 충분히 있다. 또한, LLM의 출력은 확률적으로 결정되기 때문에, 기대되는 항목명과 실제 항목명이 괴리되어 있는 경우에는 특히 LLM이 예기치 않은 내용의 텍스트를 출력할 가능성에도 주의해야 한다.

다음으로, 로컬의 YAML 파일을 Python 측에서 읽어 LLM에 전달하는 작업을 수행한다. 이 검증은 "LLM이 계산 화학 워크플로를 처음부터 설계할 수 있는가"가 아니라, "사양서·도구 정의·입력 CSV 정보를 바탕으로 하류 모듈(downstream module)이 읽을 수 있는 구조화된 JSON으로 변환할 수 있는가"를 평가하는 것이다.

여기서는 LLM에게 계산 화학 워크플로를 직접 실행하게 하는 것이 아니라, 워크플로를 나타내는 JSON을 생성하게 하는 구성으로 하였다. 따라서 LLM에 전달할 입력 파일로 다음을 준비했다.

- `planned_workflow.yaml`: 인간이 의도하는 계산 절차를 기술한 파일
- `canonical_workflow.yaml`: LLM 생성 JSON이 만족해야 하는 정규화 사양 및 검증 조건을 기술한 파일
- `tool_registry.yaml`: LLM이 사용 가능한 도구 이름, 입출력, 파라미터를 정의한 파일

이 중 `canonical_workflow.yaml`은 완전한 정답 JSON 그 자체는 아니지만, LLM이 생성해야 할 workflow JSON의 "정답에 가까운 사양"을 규정하는 파일이다. LLM이 이를 올바르게 읽을 수 있다면 어느 정도 견고한 데이터 입출력이 실현된다.

아래에 나타내는 `eval4.py` 스크립트에서는 Python 측에서 `planned_workflow.yaml`, `canonical_workflow.yaml`, `tool_registry.yaml` 및 `examples/smiles_list.csv`를 읽어 들여, CSV 헤더와 첫 번째 행을 LLM에 전달하고 있다.

## yaml 파일의 내용

workflow_id: smiles_to_xtb_relax
description: >
Generate 3D structures from a SMILES CSV, optimize each structure using xtb,
...

workflow_id: smiles_to_xtb_relax
objective: >
Convert a SMILES CSV into initial 3D structures, optimize them with xtb,
...

tools:

  • name: rdkit_generate_3d
    version: "0.1.0"
    ...

import csv
import json
from pathlib import Path
...


`build_payload()`는 LLM에 완전한 정답 JSON을 전달하는 것은 아니지만, 각 yaml 파일과 출력 Schema를 한꺼번에 전달하기 때문에 LLM은 상당히 강하게 제약된 상태에서 workflow JSON을 생성하고 있다.

이하에서는 `raw model output` (LLM이 실제로 반환한 JSON 문자열 그 자체)에 이어 `validated output` (Pydantic의 `WorkflowSpec`을 통과시킨 후, Python 측에서 정규화하여 재출력한 것)을 출력한다.

$ python eval4.py
=== raw model output ===
{
...


이를 보면 대체로 양호한 정밀도로 규정된 JSON 형식을 반환하고 있다. 한편, `name_column`이 `null`로 되어 있는 등 CSV 열 정보의 일부가 충분히 유지되지 않은 부분도 보인다.

이유로는 다음과 같은 점들을 생각할 수 있다.

- JSON Schema에서 `name_column`이나 `metadata_columns`가 임의 항목(optional item)이며, 지정하지 않아도 검증(validation)을 통과함
- `name`이라는 단어가 분자명, 파일명, workflow명 등 여러 의미를 가질 수 있음
- 태스크의 목적이 "모든 CSV 열 정보의 취득·유지"가 아니라 "계산 워크플로 JSON의 생성"으로 정의되어 있음
- `name`, `category`, `notes`와 같은 보조 열이 계산 실행에 직접적으로 필요한 열로서 강하게 지정되지 않았음

실제로 `id`, `smiles`, `charge`

、`spin_multiplicity`와 같이 계산 실행에 직결되는 열 정보는 유지되고 있다. 반면, `name`, `category`, `notes`와 같은 보조적인 열 정보는 Schema (스키마) 상 필수 사항이 아니며, 프롬프트 (prompt) 상의 우선순위도 낮았기 때문에 생략된 것으로 생각된다.

이 결과는 단순한 모델의 성능 부족이라기보다, Schema (스키마) 설계, 프롬프트 (prompt) 설계, 열 이름의 모호성, 태스크 (task)의 목적에 따른 영향이 크다. 모든 CSV 열을 확실하게 유지하고 싶다면, 프롬프트 (prompt)에서 명시할 뿐만 아니라, `name_column`이나 `metadata_columns`를 필수화하거나, Python 측에서 CSV 열 매핑을 결정론적 (deterministic)으로 생성하여 LLM에 전달하는 설계가 바람직하다.

검증 5에서는 LLM이 생성한 workflow JSON을 별도의 모듈에서 받아 실행 가능한 형식인지 확인한다. 구체적으로는, 생성된 JSON을 그대로 실제 계산에 전달하는 것이 아니라, 먼저 Pydantic 모델과 `tool_registry.yaml`에 의해 검증하고 dry-run (드라이 런)을 수행한다. 나아가 실제 운용에 가까운 조건으로서, 약 40개의 더미 (dummy) 모듈을 포함하는 large 버전의 tool registry를 준비하여, 다수의 후보 중에서 LLM이 올바른 모듈의 조합과 순서를 선택할 수 있는지 검증한다.

여기서는 사양을 명시적으로 제공하는 경우부터 자연어에 의한 프롬프트 (prompt)만 사용하는 경우까지, 다음과 같은 3단계로 나누어 평가한다.

- **Level 1: guided test (가이드 테스트)**
`expected_tools.json`을 LLM에도 전달하여, 필요 도구, 권장 순서, 기대 파라미터 (parameter) 값을 명시한 상태에서 workflow JSON을 생성하게 하는 검증. LLM의 완전 자율적인 도구 선택 능력이 아니라, 사양서·tool registry·제약 조건에 따라 안정적인 JSON을 생성할 수 있는지를 본다.
- **Level 2: semi-blind test (세미 블라인드 테스트)**
`expected_tools.json`은 LLM에 전달하지 않고, validator (검증기) 측의 채점 기준으로만 사용하는 검증. LLM에는 `planned_workflow_large.yaml`, CSV 정보, tool registry만을 전달하여, 계산 목적과 registry의 설명으로부터 필요한 도구를 선택할 수 있는지 본다.
- **Level 3: natural-language planning test (자연어 계획 테스트)**
`planned_workflow_large.yaml`도 사용하지 않고, 인간의 자연어 요청, CSV 정보, tool registry만을 LLM에 전달하는 검증. 실제 운용에 가장 가까우며, 자연어 요구사항으로부터 필요 모듈을 자율적으로 선택하여 실행 가능한 workflow JSON을 구성할 수 있는지 본다.

## yaml, json 파일 내용

- `tool_registry_large.yaml`: LLM이 사용할 수 있는 도구 목록을 정의하는 파일. 각 도구의 입력, 출력, 파라미터 (parameter)를 기술한다.
- `planned_workflow_large.yaml`: 인간이 의도하는 계산 목적을 기술하는 파일. Level 2에서 LLM에 전달한다.
- `expected_tools.json`: 채점용 기대 사양을 기술하는 파일. Level 1에서는 LLM에도 전달하며, Level 2/3에서는 validator (검증기) 측에서만 사용한다.
- `examples/smiles_list.csv`: 검증용 SMILES CSV. 열 정보를 LLM에 전달하여 workflow JSON에 반영할 수 있는지 본다.
- `user_request.txt`: Level 3에서 사용하는 자연어 요청 파일. 인간의 요청문으로부터 도구를 선택할 수 있는지 검증한다.
- `generated_large_registry_workflow.json`: LLM 생성 결과를 저장한 JSON. 재검증이나 비교에 사용한다.

id,name,smiles,charge,spin_multiplicity,category,notes
mol_001,methane,C,0,1,closed_shell,simple neutral molecule
mol_002,ethanol,CCO,0,1,closed_shell,common organic molecule
...

{
"required_tools": [
"read_smiles_csv",
...


Level 2 이후의 검증에서는 `planned_workflow_large.yaml`에서 다음을 삭제한다:

- required_tools:
- preferred_step_order:
- parameter_policy:

workflow_id: smiles_to_xtb_relax_large_registry_test
description: >
Generate initial 3D structures from a SMILES CSV, optimize them with xtb,
...

tools:

  • name: read_smiles_csv
    version: "0.1.0"
    ...

import csv
import json
import math
...


Level 1의 출력 결과를 아래에 나타낸다.

## 검증 5; Level 1의 출력 결과

=== raw model output ===
{
"workflow_id": "smiles_to_xtb_relax_large_registry_test",
...


결과적으로, level 1의 registry validation은 OK가 되었다. workflow는 `read_smiles_csv` 

→ `rdkit_validate_smiles` 

→ `rdkit_generate_3d` 

→ `xtb_optimize` 

→ `collect_results` 

의 순서로, 각 도구는 모두 `tool_registry.yaml`에 등록되어 있었다. 입출력 키(input/output key)도 `molecule_table`, `validated_molecule_table`, `initial_xyz_files`, `optimized_xyz_files`로 되어 있어 문제가 없다.

앞서 언급한 바와 같이, 이것은 guided test가 되고 있다는 점에 유의해야 한다.

`tool_registry.yaml`에서 정의하고 있는 도구는 42종류뿐이라는 점을 유의해야 한다. 실용적으로는 LLM이 선택할 수 있는 계산 도구·모듈 군이 수백 개 이상이 될 것으로 생각된다. 그러한 경우에 적절한 것을 선택할 수 있을지는 자명하지 않다.

이어서, Level 2 검증을 실시했다.

Level 2에서는 `expected_tools.json`을 LLM에게 전달하지 않고, 추가로 `planned_workflow_large.yaml`에서 `required_tools`, `preferred_step_order`, `parameter_policy`를 삭제했다. 따라서 LLM은 정답이 되는 도구 정보를 직접 보지 않고, 계산 목적, CSV 정보, large 버전의 tool registry만으로 필요한 모듈을 선택해야 한다.

## 검증 5; Level 2의 출력 결과

=== raw model output ===
{
"workflow_id": "smiles_to_xtb_relax_large_registry_test",
...


결과적으로, LLM은 `read_smiles_csv`, `rdkit_validate_smiles`, `rdkit_generate_3d`, `xtb_optimize`, `collect_results` 5개를 올바른 순서로 선택하는 데 성공했다. 불필요한 descriptor, DFT, NEB, MLIP, docking, 분자 편집 계열 모듈은 혼입되지 않았으며, step 간의 input/output도 자연스럽게 연결되어 있었다. 이 점에서는 large registry로부터의 자율적인 도구 선택은 성공했다고 평가할 수 있다.

한편, validator는 `NG / REVIEW`를 반환했다. 원인은 도구 선택이 아니라, 파라미터 값의 일탈이다. NG 사례는 다음과 같다:

- `xtb_optimize.method`가 registry enum인 `gfn2-xtb`가 아니라 `GFN2-xTB`로 출력됨 (표기 불일치)
- `rdkit_generate_3d.random_seed`가 integer(정수) 기대값에 대해 `null`이 됨 (타입 불일치)
- `num_conformers`, `max_embed_attempts`, `max_steps`, `fmax`, `keep_intermediate_runs` 등도 기대되는 값에서 벗어나 있었다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0