D-2 수정 작업: OpenAI API 우선 없으면 Ollama 적용

This commit is contained in:
mcchae committed 2026-10-12 22:29:38 +09:00
1 parent 5eedbdd039
commit 2fe0dc8de1
16 files changed
+466 -218

No files matched your search

+15 -7
View File
@@ -6,12 +6,12 @@
## 1. 설치 확인 (5분) ## 1. 설치 확인 (5분)
[사전 준비 안내](SETUP-WSL-OLLAMA.md)를 마친 상태에서 WSL 터미널에서 실행합니다. [사전 준비 안내](SETUP-WSL-LLM.md)를 마친 상태에서 WSL 터미널에서 실행합니다.
```bash ```bash
cd ~/<수업 저장소>/Last3Mon/64/workflow-lab cd ~/<수업 저장소>/Last3Mon/64/workflow-lab
uv run pytest -q # 12 passed, 1 skipped uv run pytest -q # 18 passed, 1 skipped
uv run miniflow check # Reply (ollama, ...) 가 나오면 준비 완료 uv run miniflow check # Reply (openai, ...) 가 나오면 준비 완료
``` ```
## 2. 가장 단순한 워크플로우 실행 (10분) ## 2. 가장 단순한 워크플로우 실행 (10분)
@@ -33,14 +33,22 @@ uv run miniflow run flows/01-llm-only.json -i text="다음 주 화요일 10시
Workflow: 01 LLM only Workflow: 01 LLM only
------------------------------------------------------------------------ ------------------------------------------------------------------------
[1/3] start input ok 0.0ms {"text": "다음 주 화요일 ..."} [1/3] start input ok 0.0ms {"text": "다음 주 화요일 ..."}
[2/3] summarize llm ok 7071.5ms "다음 주 화요일 10시에 기획 회의가 진행됩니다. 각 팀은 ..." [2/3] summarize llm ok 1645.8ms "다음 주 화요일 10시에 기획 회의가 있습니다. 각 팀은 진행 현황을 준비해 주세요."
[3/3] result output ok 0.1ms "다음 주 화요일 10시에 기획 회의가 진행됩니다. 각 팀은 ..." [3/3] result output ok 0.0ms "다음 주 화요일 10시에 기획 회의가 있습니다. 각 팀은 진행 현황을 준비해 주세요."
------------------------------------------------------------------------ ------------------------------------------------------------------------
Status: completed Status: completed
Trace saved: runs/20261014-101500-01-llm-only.json Trace saved: runs/20261014-101500-01-llm-only.json
``` ```
노트북의 Ollama 모델(`gemma3:4b`)이 요약을 만들었습니다. 요약 문장과 소요 시간은 실행할 때마다, 노트북마다 다릅니다. OpenAI의 `gpt-4.1-mini`가 요약을 만들었습니다(약 1.4~1.6초). 요약 문장과 소요 시간은 실행할 때마다 조금씩 다를 수 있습니다.
같은 명령을 **대체 모델(Ollama)** 로도 실행해 봅니다. `.env`보다 명령 앞에 적은 값이 우선합니다.
```bash
MINIFLOW_LLM=ollama uv run miniflow run flows/01-llm-only.json -i text="다음 주 화요일 10시에 기획 회의를 진행합니다. 각 팀은 진행 현황을 준비해 주세요."
```
수업 전 측정에서 `gemma3:4b`는 12.7초가 걸렸고, 원문에 없는 "회의에서 진행 상황을 공유하고 논의할 예정입니다"를 덧붙였습니다. 요약 노드도 **내용을 지어낼 수 있습니다**.
같은 명령을 **Mock LLM**으로도 실행해 비교해 봅니다. Mock은 노드에 적어 둔 정해진 답을 돌려줍니다. 같은 명령을 **Mock LLM**으로도 실행해 비교해 봅니다. Mock은 노드에 적어 둔 정해진 답을 돌려줍니다.
@@ -54,7 +62,7 @@ MINIFLOW_LLM=mock uv run miniflow run flows/01-llm-only.json -i text="다음 주
1. `summarize` 노드의 `prompt` 필드에는 무엇이 들어 있나요? `{{input.text}}`는 어떻게 바뀌었나요? 1. `summarize` 노드의 `prompt` 필드에는 무엇이 들어 있나요? `{{input.text}}`는 어떻게 바뀌었나요?
2. `provider` 값은 무엇인가요? Mock으로 실행한 기록과 비교하면 무엇이 다른가요? 2. `provider` 값은 무엇인가요? Mock으로 실행한 기록과 비교하면 무엇이 다른가요?
3. 각 노드의 `ms` 값을 보면 어느 노드가 가장 오래 걸렸나요? Mock으로 실행했을 때와 비교하면 얼마나 차이 나나요? 3. 각 노드의 `ms` 값을 보면 어느 노드가 가장 오래 걸렸나요? Mock, Ollama로 실행했을 때와 비교하면 얼마나 차이 나나요?
```bash ```bash
uv run miniflow show flows/01-llm-only.json uv run miniflow show flows/01-llm-only.json
+16 -15
View File
@@ -13,7 +13,7 @@ uv run miniflow run flows/02-llm-tool.json -i city=제주 -i question="우산이
```text ```text
[2/4] weather tool ok 0.1ms {"city": "제주", "condition": "비", "temp_c": 20, "rain_chance": 80, ...} [2/4] weather tool ok 0.1ms {"city": "제주", "condition": "비", "temp_c": 20, "rain_chance": 80, ...}
[3/4] answer llm ok 3783.8ms "네, 비가 오므로 우산이 필요할 것 같습니다." [3/4] answer llm ok 1426.2ms "네, 제주에 비가 오고 비 올 확률이 80%이므로 우산이 필요합니다."
``` ```
확인할 것 확인할 것
@@ -71,36 +71,37 @@ uv run miniflow run flows/my-02-with-count.json -i city=서울 -i question="산
`validate`가 오류를 내면 오류 메시지를 읽고 고칩니다. 대부분 노드 id 오타나 연결 누락입니다. `validate`가 오류를 내면 오류 메시지를 읽고 고칩니다. 대부분 노드 id 오타나 연결 누락입니다.
## 4. Mock과 실제 LLM 비교 (10분) ## 4. Mock·OpenAI·대체 모델 비교 (10분)
지금까지는 Ollama 모델이 답했습니다. 같은 워크플로우를 Mock과 번갈아 실행해 비교합니다. 지금까지는 OpenAI `gpt-4.1-mini`가 답했습니다. 같은 워크플로우를 Mock, 대체 모델(Ollama)과 번갈아 실행해 비교합니다.
```bash ```bash
uv run miniflow run flows/05-tech-support-ticket.json -i user=kim -i ticket="프린터에서 출력이 안 됩니다" uv run miniflow run flows/05-tech-support-ticket.json -i user=kim -i ticket="프린터에서 출력이 안 됩니다"
uv run miniflow run flows/05-tech-support-ticket.json -i user=kim -i ticket="프린터에서 출력이 안 됩니다" uv run miniflow run flows/05-tech-support-ticket.json -i user=kim -i ticket="프린터에서 출력이 안 됩니다"
MINIFLOW_LLM=mock uv run miniflow run flows/05-tech-support-ticket.json -i user=kim -i ticket="프린터에서 출력이 안 됩니다" MINIFLOW_LLM=mock uv run miniflow run flows/05-tech-support-ticket.json -i user=kim -i ticket="프린터에서 출력이 안 됩니다"
MINIFLOW_LLM=ollama uv run miniflow run flows/05-tech-support-ticket.json -i user=kim -i ticket="프린터에서 출력이 안 됩니다"
``` ```
| 비교할 것 | Mock | Ollama | | 비교할 것 | Mock | OpenAI | Ollama |
| --- | --- | --- | | --- | --- | --- | --- |
| `classify`·`draft` 노드의 `ms` | | | | `classify`·`draft` 노드의 `ms` | | | |
| 두 번 실행한 답변이 같은가? | | | | 두 번 실행한 답변이 같은가? | | | |
| 분류 결과가 정해진 값(`device`)과 정확히 같은가? | | | | 분류 결과가 정해진 값(`device`)과 정확히 같은가? | | | |
수업 전에 확인한 결과(CPU 1개, 메모리 8GB 환경)는 다음과 같았습니다. 내 노트북 결과와 비교해 보세요. 수업 전에 같은 문의 4건(비밀번호, VPN, 프린터, 메일 프로그램 설치)으로 확인한 결과입니다. 내 노트북 결과와 비교해 보세요. Ollama 시간은 노트북 성능에 따라 크게 다릅니다.
| 모델 | 분류 4건 정답 | 분류 노드 | 답변 초안 노드 | | 모델 | 분류 4건 정답 | 분류 노드 | 답변 초안 노드 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `gemma3:1b` | 0/4 (전부 `software`, 한 번은 `Software`) | 약 1초 | 4~7초 | | `gpt-4.1-mini` (OpenAI, 기본) | 4/4 | 약 1~2.6초 | 약 1.2~1.7초 |
| `gemma3:4b` | 4/4 | 약 1.7초 | 7~11초 | | `gemma3:4b` (대체) | 4/4 | 약 1.6~2.3초 | 7~11초 |
| `gemma3:1b` (8GB 노트북 대체) | 1/4 (나머지 `software`, 대문자 `Software`가 나온 적도 있음) | 약 1초 | 4~7초 |
작은 모델은 빠르지만 판단이 틀리거나 대소문자처럼 형식이 흔들립니다. 다음 노드가 분류값을 비교해야 한다면, LLM 출력을 그대로 믿지 말고 **검증하는 단계**가 필요합니다. (73일차 입력·출력 검증) 작은 모델은 빠르지만 판단이 틀리거나 대소문자처럼 형식이 흔들립니다. 8GB 노트북은 `OLLAMA_MODEL=gemma3:1b MINIFLOW_LLM=ollama`로, 메모리가 넉넉하면 두 모델을 모두 돌려 봅니다. 다음 노드가 분류값을 비교해야 한다면, LLM 출력을 그대로 믿지 말고 **검증하는 단계**가 필요합니다. (73일차 입력·출력 검증)
(선택) OpenAI/Anthropic/Gemini 중 API 키가 있으면 클라우드 모델과도 비교할 수 있습니다. 가지고 있는 키 하나만 설정하면 됩니다. (선택) Anthropic·Gemini 키가 있으면 다른 제공사 모델과도 비교할 수 있습니다. 가지고 있는 키 하나만 설정하면 됩니다.
| 제공사 | 설치 | 설정 | | 제공사 | 설치 | 설정 |
| --- | --- | --- | | --- | --- | --- |
| OpenAI | `uv sync --extra openai` | `export MINIFLOW_LLM=openai`, `export OPENAI_API_KEY=...` |
| Anthropic | `uv sync --extra anthropic` | `export MINIFLOW_LLM=anthropic`, `export ANTHROPIC_API_KEY=...` | | Anthropic | `uv sync --extra anthropic` | `export MINIFLOW_LLM=anthropic`, `export ANTHROPIC_API_KEY=...` |
| Gemini (10일차에 발급한 키) | `uv sync --extra gemini` | `export MINIFLOW_LLM=gemini`, `export GEMINI_API_KEY=...` | | Gemini (10일차에 발급한 키) | `uv sync --extra gemini` | `export MINIFLOW_LLM=gemini`, `export GEMINI_API_KEY=...` |
@@ -117,4 +118,4 @@ MINIFLOW_LLM=mock uv run miniflow run flows/05-tech-support-ticket.json -i user=
- [ ] Tool 결과가 LLM 프롬프트에 들어간 것을 실행 기록에서 찾았다. - [ ] Tool 결과가 LLM 프롬프트에 들어간 것을 실행 기록에서 찾았다.
- [ ] Memory가 두 번의 실행을 이어 주는 것을 확인했다. - [ ] Memory가 두 번의 실행을 이어 주는 것을 확인했다.
- [ ] `my-02-with-count.json`을 만들어 `validate`와 `run`을 통과했다. - [ ] `my-02-with-count.json`을 만들어 `validate`와 `run`을 통과했다.
- [ ] Mock과 Ollama의 결과·속도 차이를 표로 정리했다. - [ ] Mock·OpenAI·Ollama의 결과·속도 차이를 표로 정리했다.
+9 -2
View File
@@ -23,7 +23,14 @@ uv run miniflow run flows/05-tech-support-ticket.json -i user=lee -i ticket="점
| 2번째 | `history` 노드 출력에 1번째 문의가 남아 있는가? | | 2번째 | `history` 노드 출력에 1번째 문의가 남아 있는가? |
| 3번째 | 어느 노드에서 왜 멈추었는가? `draft` 노드는 무엇이라고 답했는가? 이 경우 실제 업무에서는 어떻게 처리해야 할까? | | 3번째 | 어느 노드에서 왜 멈추었는가? `draft` 노드는 무엇이라고 답했는가? 이 경우 실제 업무에서는 어떻게 처리해야 할까? |
3번째 실행에서는 FAQ 검색 결과가 없는데도 LLM이 그럴듯한 답변 초안을 만들고, 마지막 `result` 노드에서 멈춥니다. Mock으로 실행(`MINIFLOW_LLM=mock`)하면 `draft` 노드에서 바로 멈춥니다. **근거가 없을 때 실제 LLM이 어떻게 행동하는지** 실행 기록에서 확인해 보세요. 3번째 실행에서는 FAQ 검색 결과가 없는데도 LLM이 답변 초안을 만들고, 마지막 `result` 노드에서 멈춥니다. Mock으로 실행(`MINIFLOW_LLM=mock`)하면 `draft` 노드에서 바로 멈춥니다. **근거가 없을 때 실제 LLM이 어떻게 행동하는지** 실행 기록에서 확인해 보세요.
| 모델 (수업 전 측정) | `draft`가 쓴 답 | `result` |
| --- | --- | --- |
| `gpt-4.1-mini` | "점심 메뉴 추천에 관한 FAQ가 현재 제공되지 않아 … 양해 부탁드립니다" (솔직하게 답함) | `'0' not found`로 실패 |
| 대체 `gemma3:4b` (`MINIFLOW_LLM=ollama`) | "… FAQ 검색 결과가 없습니다. (FAQ ID: other-123)" (**없는 FAQ 번호를 지어냄**) | `'0' not found`로 실패 |
좋은 모델은 근거가 없다고 말했지만, 워크플로우는 어느 쪽이든 마지막에 가서야 멈춥니다. 모델이 바뀌어도 안전하려면 "FAQ가 없으면 담당자에게" 같은 **경로**가 필요합니다 (65일차 Condition).
`uv run miniflow show flows/05-tech-support-ticket.json`으로 구조도 확인합니다. `uv run miniflow show flows/05-tech-support-ticket.json`으로 구조도 확인합니다.
@@ -82,7 +89,7 @@ uv run miniflow run flows/my-scenario.json -i user=me -i text="..."
Tip Tip
- LLM 노드의 프롬프트에는 답의 형식(정해진 값, 문장 수 등)을 분명히 적습니다. Ollama 답이 매번 달라 흐름 확인이 어려우면 `mock`의 `rules`·`template`을 함께 적고 `MINIFLOW_LLM=mock`으로 구조부터 확인합니다. - LLM 노드의 프롬프트에는 답의 형식(정해진 값, 문장 수 등)을 분명히 적습니다. LLM 답이 매번 달라 흐름 확인이 어려우면 `mock`의 `rules`·`template`을 함께 적고 `MINIFLOW_LLM=mock`으로 구조부터 확인합니다.
- 사용할 수 있는 Tool은 `uv run miniflow tools`로 확인합니다 (`get_weather`, `search_faq`, `count_chars`). - 사용할 수 있는 Tool은 `uv run miniflow tools`로 확인합니다 (`get_weather`, `search_faq`, `count_chars`).
- `show` 결과를 분석 문서의 "구현한 워크플로우" 항목에 붙여 넣습니다. - `show` 결과를 분석 문서의 "구현한 워크플로우" 항목에 붙여 넣습니다.
+6 -5
View File
@@ -10,7 +10,7 @@
```text ```text
Last3Mon/64/ Last3Mon/64/
├── README.md ├── README.md
├── SETUP-WSL-OLLAMA.md # 사전 준비 · WSL과 Ollama 로컬 LLM ├── SETUP-WSL-LLM.md # 사전 준비 · WSL, OpenAI API 키(.env), 대체 Ollama
├── LAB-01-FIRST-WORKFLOW.md # 1단계 · 첫 워크플로우 실행과 실행 기록 읽기 ├── LAB-01-FIRST-WORKFLOW.md # 1단계 · 첫 워크플로우 실행과 실행 기록 읽기
├── LAB-02-LLM-TOOL-MEMORY.md # 2단계 · LLM·Tool·Memory 연결하기 ├── LAB-02-LLM-TOOL-MEMORY.md # 2단계 · LLM·Tool·Memory 연결하기
├── LAB-03-SCENARIO-ANALYSIS.md # 3단계 · 업무 자동화 시나리오 분석과 설계 ├── LAB-03-SCENARIO-ANALYSIS.md # 3단계 · 업무 자동화 시나리오 분석과 설계
@@ -41,11 +41,12 @@ flowchart LR
## 준비 (수업 전날) ## 준비 (수업 전날)
실습은 Windows 11의 WSL(Ubuntu)에서, LLM은 노트북에서 실행하는 Ollama(`gemma3:4b`, 8GB 노트북은 `gemma3:1b`)로 진행합니다. API 키는 필요 없습니다. 실습은 Windows 11의 WSL(Ubuntu)에서 진행합니다. LLM은 OpenAI API(`gpt-4.1-mini`, 키는 `Last3Mon/.env`)를 쓰고, 키·인터넷에 문제가 있으면 노트북의 Ollama(`gemma3:4b`, 8GB 노트북은 `gemma3:1b`)가 대신 답합니다.
먼저 **[실습 준비 안내](../../Lecture/64/00-DAY64-PREP.md)** 로 전체 준비 항목을 확인하고, 설치는 **[SETUP-WSL-OLLAMA.md](SETUP-WSL-OLLAMA.md)** 의 순서대로 진행한 뒤 마지막에 아래 두 명령이 통과하는지 확인합니다. 먼저 **[실습 준비 안내](../../Lecture/64/00-DAY64-PREP.md)** 로 전체 준비 항목을 확인하고, 설치는 **[SETUP-WSL-LLM.md](SETUP-WSL-LLM.md)** 의 순서대로 진행한 뒤 마지막에 아래 명령이 통과하는지 확인합니다.
```bash ```bash
uv run pytest -q # 12 passed, 1 skipped uv run pytest -q # 18 passed, 1 skipped
uv run miniflow check # Reply (ollama, ...) uv run miniflow check # Reply (openai, ...)
OPENAI_API_KEY=sk-wrong uv run miniflow check # Reply (ollama, ...) : 대체 경로
``` ```
+181
View File
@@ -0,0 +1,181 @@
# 사전 준비 · WSL 실습 환경과 LLM (OpenAI API 기본, Ollama 대체)
64일차 실습은 Windows 11의 **WSL(Ubuntu)** 에서 진행합니다. LLM은 **OpenAI API**(기본 모델 `gpt-4.1-mini`)를 쓰고, 키가 없거나 인터넷·API에 문제가 있으면 노트북에서 직접 실행하는 **Ollama**(`gemma3:4b`)가 대신 답합니다. 이 구성은 65일차 이후 모든 실습에서 그대로 씁니다.
> Ollama 모델 파일이 1~3GB입니다. 수업 중에 21명이 동시에 받으면 강의실 네트워크가 느려지므로 **수업 전날 5단계(모델 다운로드)까지 끝내 주세요.**
```mermaid
flowchart LR
subgraph WIN["Windows 11"]
T["Windows Terminal"]
subgraph WSL["WSL2 · Ubuntu"]
E[".env<br/>OPENAI_API_KEY<br/>(git 제외)"] -.-> M
M["miniflow<br/>(uv 프로젝트)"] -.->|"대체<br/>localhost:11434"| O["Ollama 서버"]
O --> G[("gemma3:4b 모델")]
end
T --> WSL
end
M -->|"기본"| AI["OpenAI API<br/>gpt-4.1-mini"]
```
## 1. WSL 확인 (5분)
PowerShell에서 확인합니다.
```powershell
wsl --list --verbose
```
`Ubuntu`가 `VERSION 2`로 보이면 됩니다. 없으면 `wsl --install -d Ubuntu`로 설치하고 노트북을 다시 시작합니다.
이후 명령은 모두 **WSL(Ubuntu) 터미널**에서 실행합니다.
## 2. 실습 코드와 uv 준비 (10분)
```bash
# uv (이미 설치되어 있으면 건너뜀)
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.bashrc
# 실습 폴더는 WSL 홈(~) 아래에 둡니다. /mnt/c 아래는 느리고 권한 문제가 생깁니다.
cd ~
# 수업 자료를 받는 방법은 강사가 따로 안내합니다.
cd ~/<수업 저장소>/Last3Mon/64/workflow-lab
uv sync
uv run pytest -q
```
`18 passed, 1 skipped`가 나오면 됩니다. (`skipped` 1개는 선택 기능인 Anthropic 연결 테스트로, 해당 패키지를 설치하지 않으면 건너뜁니다.) 테스트는 `.env`를 읽지 않고 Mock LLM으로만 돌기 때문에 키가 없어도 통과합니다.
## 3. OpenAI API 키를 `.env`에 넣기 (3분)
과정에서 받은 OpenAI API 키를 `Last3Mon/.env` 한 곳에 넣습니다. `miniflow`는 실행한 폴더에서 위로 올라가며 가장 가까운 `.env`를 읽으므로, 64일차부터 모든 일차가 이 파일 하나를 씁니다.
```bash
cd ~/<수업 저장소>/Last3Mon
cp .env.example .env
nano .env # OPENAI_API_KEY= 뒤에 키를 붙여 넣고 저장 (Ctrl+O, Enter, Ctrl+X)
```
| 줄 | 뜻 |
| --- | --- |
| `OPENAI_API_KEY=` | 받은 키 (`sk-`로 시작). **문서·워크플로우·채팅·화면 캡처에 올리지 않습니다** |
| `OPENAI_MODEL=gpt-4.1-mini` | 수업 기본 모델 |
| `MINIFLOW_LLM=openai` | `miniflow`가 OpenAI를 씀. 키를 받지 못했으면 `ollama`로 바꿈 |
| `MINIFLOW_LLM_FALLBACK=ollama` | OpenAI 호출이 실패하면 Ollama로 한 번 더 시도 |
| `OLLAMA_MODEL=gemma3:4b` | 대체 모델. 8GB 노트북은 `gemma3:1b` |
> `.env`는 `.gitignore`에 들어 있어 커밋되지 않습니다. 키가 들어간 파일을 다른 이름으로 복사해 두지 마세요.
>
> 셸에 `export MINIFLOW_LLM=...`을 적어 두었다면(`~/.bashrc`) 그 값이 `.env`보다 우선합니다. 지난 실습에서 넣은 줄이 있으면 지우고 터미널을 다시 엽니다.
## 4. 노트북 메모리에 맞는 대체 모델 고르기
WSL은 기본으로 **Windows 전체 메모리의 절반**까지만 사용합니다.
| 노트북 메모리 | WSL이 쓸 수 있는 메모리 | 대체 모델 (`OLLAMA_MODEL`) |
| --- | --- | --- |
| 8GB | 약 4GB | `gemma3:1b` |
| 16GB 이상 | 약 8GB 이상 | `gemma3:4b` (권장) |
내 노트북 메모리는 PowerShell에서 확인합니다.
```powershell
(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB
```
메모리가 부족하다는 오류가 나면 `C:\Users\<사용자>\.wslconfig` 파일을 만들어 상한을 올린 뒤 PowerShell에서 `wsl --shutdown` 하고 WSL을 다시 엽니다.
```ini
[wsl2]
memory=10GB
swap=4GB
```
## 5. Ollama 설치와 모델 다운로드 (10~20분)
OpenAI가 기본이지만 Ollama는 **꼭** 준비합니다. 키·인터넷에 문제가 있을 때 대신 답하고, 2단계 실습에서 모델 크기에 따른 차이를 직접 비교합니다.
```bash
curl -fsSL https://ollama.com/install.sh | sh
ollama --version
# 메모리 16GB 이상
ollama pull gemma3:4b
# 메모리 8GB
ollama pull gemma3:1b
ollama list
```
`ollama list`에 모델이 보이면 됩니다. 설치 스크립트가 Ollama 서버를 자동으로 실행합니다. `ollama list`에서 연결 오류가 나면 다른 터미널 창에서 `ollama serve`를 실행해 두세요.
## 6. 연결 확인 (5분)
```bash
cd ~/<수업 저장소>/Last3Mon/64/workflow-lab
uv run miniflow check
```
```text
MINIFLOW_LLM : openai
.env : /home/<사용자>/<수업 저장소>/Last3Mon/.env
OPENAI_MODEL : gpt-4.1-mini
API key : set
OLLAMA_HOST : http://localhost:11434
OLLAMA_MODEL : gemma3:4b
FALLBACK : ollama
Reply (openai, 2.0s): ready
```
대체 경로도 한 번 확인합니다. 키를 일부러 틀리게 넣으면(이 명령에서만) Ollama가 대신 답합니다.
```bash
OPENAI_API_KEY=sk-wrong uv run miniflow check
```
```text
[miniflow] openai failed (OpenAI rejected the API key (check OPENAI_API_KEY in .env)); trying ollama
Reply (ollama, 20.9s): Ready
```
Ollama의 첫 호출은 모델을 메모리에 올리느라 10~20초 걸립니다. 두 번째부터는 빨라집니다.
## 7. 이미 Windows나 Docker에 Ollama가 있는 경우
9월 실습에서 Docker로 Ollama를 실행해 둔 경우에는 WSL에 새로 설치하지 않고 그대로 써도 됩니다.
| 상황 | 설정 |
| --- | --- |
| Docker Desktop으로 실행한 Ollama (`11434` 포트 공개) | 컨테이너를 켜 두면 WSL에서도 `http://localhost:11434`로 연결됩니다. 6절의 대체 경로 확인으로 점검 |
| Windows용 Ollama 앱 | `.wslconfig`에 `networkingMode=mirrored`를 추가하고 `wsl --shutdown` 후 다시 열면 `localhost`로 연결됩니다 |
```ini
[wsl2]
networkingMode=mirrored
```
## 8. 문제 해결
| 증상 | 해결 |
| --- | --- |
| `API key : MISSING` | `Last3Mon/.env`에 `OPENAI_API_KEY`가 비어 있음. 키를 넣거나, 키가 없으면 `MINIFLOW_LLM=ollama` |
| `.env : (none)` | `.env` 위치 확인 (`Last3Mon/.env`), 실습 폴더 안에서 실행했는지 확인 |
| `OpenAI rejected the API key` | 키를 잘못 붙여 넣음 (앞뒤 공백·줄바꿈 확인) |
| `HTTP 429 ... insufficient_quota` | 키의 크레딧이 없음. 강사에게 알리고 그동안 `MINIFLOW_LLM=ollama` |
| `Cannot reach the OpenAI API` | 인터넷·프록시 문제. 그동안 `MINIFLOW_LLM=ollama` |
| `MINIFLOW_LLM : ollama`로 나옴 | `~/.bashrc`의 `export MINIFLOW_LLM=ollama`가 `.env`보다 우선. 그 줄을 지우고 터미널 다시 열기 |
| `Cannot reach Ollama at http://localhost:11434` | `ollama serve` 실행, 또는 7절의 연결 설정 확인 |
| `Model 'gemma3:4b' not found on Ollama` | `ollama pull gemma3:4b` |
| Ollama 응답이 1분 넘게 걸림, WSL이 멈춤 | 메모리 부족. `gemma3:1b`로 바꾸거나 `.wslconfig`의 `memory` 상향 |
| `uv: command not found` | `source ~/.bashrc` 또는 터미널 다시 열기 |
| 한글이 깨져 보임 | Windows Terminal 사용 (기본 UTF-8) |
| OpenAI와 Ollama 모두 안 됨 | `export MINIFLOW_LLM=mock`으로 Mock LLM 사용 (결과는 정해진 답) |
## 확인
- [ ] `uv run pytest -q` → `18 passed, 1 skipped`
- [ ] `Last3Mon/.env`에 키를 넣었다 (키는 이 파일에만 있다)
- [ ] `ollama list`에 모델이 보인다
- [ ] `uv run miniflow check`에서 `Reply (openai, ...)`가 나온다
- [ ] `OPENAI_API_KEY=sk-wrong uv run miniflow check`에서 `Reply (ollama, ...)`가 나온다
-144
View File
@@ -1,144 +0,0 @@
# 사전 준비 · WSL에서 실습 환경과 Ollama 로컬 LLM 준비
64일차 실습은 Windows 11의 **WSL(Ubuntu)** 에서 진행하고, LLM은 노트북에서 직접 실행하는 **Ollama**를 사용합니다. API 키가 필요 없고 사용량 제한도 없습니다.
> 모델 파일이 1~3GB입니다. 수업 중에 21명이 동시에 받으면 강의실 네트워크가 느려지므로 **수업 전날 4단계(모델 다운로드)까지 끝내 주세요.**
```mermaid
flowchart LR
subgraph WIN["Windows 11"]
T["Windows Terminal"]
subgraph WSL["WSL2 · Ubuntu"]
M["miniflow<br/>(uv 프로젝트)"] -->|"http://localhost:11434"| O["Ollama 서버"]
O --> G[("gemma3:4b 모델")]
end
T --> WSL
end
```
## 1. WSL 확인 (5분)
PowerShell에서 확인합니다.
```powershell
wsl --list --verbose
```
`Ubuntu`가 `VERSION 2`로 보이면 됩니다. 없으면 `wsl --install -d Ubuntu`로 설치하고 노트북을 다시 시작합니다.
이후 명령은 모두 **WSL(Ubuntu) 터미널**에서 실행합니다.
## 2. 노트북 메모리에 맞는 모델 고르기
WSL은 기본으로 **Windows 전체 메모리의 절반**까지만 사용합니다.
| 노트북 메모리 | WSL이 쓸 수 있는 메모리 | 사용할 모델 |
| --- | --- | --- |
| 8GB | 약 4GB | `gemma3:1b` |
| 16GB 이상 | 약 8GB 이상 | `gemma3:4b` (권장) |
내 노트북 메모리는 PowerShell에서 확인합니다.
```powershell
(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB
```
메모리가 부족하다는 오류가 나면 `C:\Users\<사용자>\.wslconfig` 파일을 만들어 상한을 올린 뒤 PowerShell에서 `wsl --shutdown` 하고 WSL을 다시 엽니다.
```ini
[wsl2]
memory=10GB
swap=4GB
```
## 3. 실습 코드와 uv 준비 (10분)
```bash
# uv (이미 설치되어 있으면 건너뜀)
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.bashrc
# 실습 폴더는 WSL 홈(~) 아래에 둡니다. /mnt/c 아래는 느리고 권한 문제가 생깁니다.
cd ~
# 수업 저장소를 받는 방법은 강의 시간에 안내합니다.
cd ~/<수업 저장소>/Last3Mon/64/workflow-lab
uv sync
uv run pytest -q
```
`12 passed, 1 skipped`가 나오면 됩니다. (`skipped` 1개는 선택 기능인 Anthropic 연결 테스트로, 해당 패키지를 설치하지 않으면 건너뜁니다.)
## 4. Ollama 설치와 모델 다운로드 (10~20분)
```bash
curl -fsSL https://ollama.com/install.sh | sh
ollama --version
# 메모리 16GB 이상
ollama pull gemma3:4b
# 메모리 8GB
ollama pull gemma3:1b
ollama list
```
`ollama list`에 모델이 보이면 됩니다. 설치 스크립트가 Ollama 서버를 자동으로 실행합니다. `ollama list`에서 연결 오류가 나면 다른 터미널 창에서 `ollama serve`를 실행해 두세요.
## 5. miniflow와 Ollama 연결 (5분)
`~/.bashrc` 맨 아래에 추가합니다. 8GB 노트북은 모델 이름을 `gemma3:1b`로 바꿉니다.
```bash
cat >> ~/.bashrc <<'EOF'
export MINIFLOW_LLM=ollama
export OLLAMA_MODEL=gemma3:4b
EOF
source ~/.bashrc
```
연결을 확인합니다.
```bash
cd ~/<수업 저장소>/Last3Mon/64/workflow-lab
uv run miniflow check
```
```text
MINIFLOW_LLM : ollama
OLLAMA_HOST : http://localhost:11434
OLLAMA_MODEL : gemma3:4b
Reply (ollama, 18.8s): Ready
```
첫 호출은 모델을 메모리에 올리느라 10~20초 걸립니다. 두 번째부터는 빨라집니다.
## 6. 이미 Windows나 Docker에 Ollama가 있는 경우
9월 실습에서 Docker로 Ollama를 실행해 둔 경우에는 WSL에 새로 설치하지 않고 그대로 써도 됩니다.
| 상황 | 설정 |
| --- | --- |
| Docker Desktop으로 실행한 Ollama (`11434` 포트 공개) | 컨테이너를 켜 두면 WSL에서도 `http://localhost:11434`로 연결됩니다. `uv run miniflow check`로 확인 |
| Windows용 Ollama 앱 | `.wslconfig`에 `networkingMode=mirrored`를 추가하고 `wsl --shutdown` 후 다시 열면 `localhost`로 연결됩니다 |
```ini
[wsl2]
networkingMode=mirrored
```
## 7. 문제 해결
| 증상 | 해결 |
| --- | --- |
| `Cannot reach Ollama at http://localhost:11434` | `ollama serve` 실행, 또는 6절의 연결 설정 확인 |
| `Model 'gemma3:4b' not found on Ollama` | `ollama pull gemma3:4b` |
| 응답이 1분 넘게 걸림, WSL이 멈춤 | 메모리 부족. `gemma3:1b`로 바꾸거나 `.wslconfig`의 `memory` 상향 |
| `uv: command not found` | `source ~/.bashrc` 또는 터미널 다시 열기 |
| 한글이 깨져 보임 | Windows Terminal 사용 (기본 UTF-8) |
| Ollama가 없거나 문제가 계속됨 | `export MINIFLOW_LLM=mock`으로 Mock LLM 사용 (결과는 정해진 답) |
## 확인
- [ ] `uv run pytest -q` → `12 passed, 1 skipped`
- [ ] `ollama list`에 모델이 보인다
- [ ] `uv run miniflow check`에서 `Reply (ollama, ...)`가 나온다
+8 -9
View File
@@ -5,7 +5,7 @@
```mermaid ```mermaid
flowchart LR flowchart LR
J["flows/*.json<br/>노드와 연결 정의"] --> E["miniflow 엔진<br/>순서 계산 · 실행"] J["flows/*.json<br/>노드와 연결 정의"] --> E["miniflow 엔진<br/>순서 계산 · 실행"]
E --> L{{"LLM 노드<br/>ollama · mock · openai · anthropic · gemini"}} E --> L{{"LLM 노드<br/>openai · ollama · mock · anthropic · gemini"}}
E --> T["Tool 노드<br/>get_weather · search_faq"] E --> T["Tool 노드<br/>get_weather · search_faq"]
E --> M[("Memory 노드<br/>.miniflow-memory.json")] E --> M[("Memory 노드<br/>.miniflow-memory.json")]
E --> R["runs/*.json<br/>실행 기록(Trace)"] E --> R["runs/*.json<br/>실행 기록(Trace)"]
@@ -57,19 +57,18 @@ uv run pytest -q
## 5. LLM 선택 ## 5. LLM 선택
`MINIFLOW_LLM` 환경 변수로 LLM을 고릅니다. 수업 기본값은 노트북에서 실행하는 **Ollama**입니다. 준비 방법은 [SETUP-WSL-OLLAMA.md](../SETUP-WSL-OLLAMA.md)를 참고하세요. `MINIFLOW_LLM`으로 LLM을 고릅니다. 보통은 `Last3Mon/.env`에 적어 두고(실행한 폴더에서 위로 올라가며 가장 가까운 `.env`를 읽음), 셸이나 명령 앞에 적은 값이 `.env`보다 우선합니다. 수업 기본값은 **OpenAI API**이고, `MINIFLOW_LLM_FALLBACK=ollama`이면 OpenAI 호출이 실패할 때 노트북의 Ollama로 한 번 더 시도합니다. 준비 방법은 [SETUP-WSL-LLM.md](../SETUP-WSL-LLM.md)를 참고하세요.
| `MINIFLOW_LLM` | LLM | 필요한 것 | | `MINIFLOW_LLM` | LLM | 필요한 것 |
| --- | --- | --- | | --- | --- | --- |
| `ollama` (수업 기본) | 로컬 모델 (`OLLAMA_MODEL`, 기본 `gemma3:4b`) | Ollama 설치, 모델 다운로드 | | `openai` (수업 기본) | OpenAI (`OPENAI_MODEL`, 기본 `gpt-4.1-mini`) | `.env`의 `OPENAI_API_KEY` (패키지는 `uv sync`에 포함) |
| `mock` (설정하지 않았을 때) | 노드의 `mock` 설정대로 정해진 답 | 없음 (자동 테스트, Ollama가 안 될 때) | | `ollama` (대체) | 로컬 모델 (`OLLAMA_MODEL`, 기본 `gemma3:4b`) | Ollama 설치, 모델 다운로드 |
| `openai` | OpenAI (`OPENAI_MODEL`) | `uv sync --extra openai`, `OPENAI_API_KEY` | | `mock` (`.env`도 설정도 없을 때) | 노드의 `mock` 설정대로 정해진 답 | 없음 (자동 테스트, 둘 다 안 될 때) |
| `anthropic` | Anthropic Claude (`ANTHROPIC_MODEL`, 기본 `claude-opus-5-5`) | `uv sync --extra anthropic`, `ANTHROPIC_API_KEY` | | `anthropic` | Anthropic Claude (`ANTHROPIC_MODEL`, 기본 `claude-opus-5-5`) | `uv sync --extra anthropic`, `ANTHROPIC_API_KEY` |
| `gemini` | Google Gemini (`GEMINI_MODEL`) | `uv sync --extra gemini`, `GEMINI_API_KEY` | | `gemini` | Google Gemini (`GEMINI_MODEL`) | `uv sync --extra gemini`, `GEMINI_API_KEY` |
```bash ```bash
export MINIFLOW_LLM=ollama uv run miniflow check # .env 설정으로 연결 확인 → Reply (openai, ...)
export OLLAMA_MODEL=gemma3:4b # 8GB 노트북은 gemma3:1b OPENAI_API_KEY=sk-wrong uv run miniflow check # 대체 경로 확인 → Reply (ollama, ...)
export OLLAMA_HOST=http://localhost:11434 # 기본값이라 생략 가능 MINIFLOW_LLM=ollama OLLAMA_MODEL=gemma3:1b uv run miniflow check # 대체 모델만 따로
uv run miniflow check # 연결 확인
``` ```
+1 -1
View File
@@ -4,7 +4,7 @@ version = "0.1.0"
description = "A tiny JSON-defined AI workflow runner for learning LLM, Tool and Memory nodes" description = "A tiny JSON-defined AI workflow runner for learning LLM, Tool and Memory nodes"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [] dependencies = ["openai>=1.40"] # course default LLM (key in .env)
[project.optional-dependencies] [project.optional-dependencies]
gemini = ["google-genai>=1.0"] gemini = ["google-genai>=1.0"]
+7 -1
View File
@@ -67,9 +67,15 @@ def cmd_check(_: argparse.Namespace) -> int:
provider = os.getenv("MINIFLOW_LLM", "mock").lower() provider = os.getenv("MINIFLOW_LLM", "mock").lower()
print(f"MINIFLOW_LLM : {provider}") print(f"MINIFLOW_LLM : {provider}")
if provider == "ollama": print(f".env : {llm.envfile.LOADED[0] if llm.envfile.LOADED else '(none)'}")
if provider == "openai":
print(f"OPENAI_MODEL : {os.getenv('OPENAI_MODEL', 'gpt-4.1-mini')}")
print(f"API key : {'set' if os.getenv('OPENAI_API_KEY') else 'MISSING (put OPENAI_API_KEY in .env)'}")
if provider == "ollama" or os.getenv("MINIFLOW_LLM_FALLBACK", "").lower() == "ollama":
print(f"OLLAMA_HOST : {os.getenv('OLLAMA_HOST', 'http://localhost:11434')}") print(f"OLLAMA_HOST : {os.getenv('OLLAMA_HOST', 'http://localhost:11434')}")
print(f"OLLAMA_MODEL : {os.getenv('OLLAMA_MODEL', 'gemma3:4b')}") print(f"OLLAMA_MODEL : {os.getenv('OLLAMA_MODEL', 'gemma3:4b')}")
if os.getenv("MINIFLOW_LLM_FALLBACK"):
print(f"FALLBACK : {os.getenv('MINIFLOW_LLM_FALLBACK')}")
started = time.perf_counter() started = time.perf_counter()
try: try:
reply, used = llm.complete({"temperature": 0}, "Reply with exactly one word: ready", "", {}) reply, used = llm.complete({"temperature": 0}, "Reply with exactly one word: ready", "", {})
@@ -0,0 +1,50 @@
"""Load settings from the nearest .env file (KEY=VALUE lines, no extra package needed).
Search order: the current folder and its parents, then the folders above this package
(so one Last3Mon/.env serves every day). Only the nearest file is read, and variables
already set in the shell win. Set MINIFLOW_DOTENV=0 to skip (the tests do this).
The .env file holds API keys: it is listed in .gitignore and must never be committed.
"""
import os
from pathlib import Path
LOADED: list[str] = [] # the .env file that was read, if any
def parse(text: str) -> dict[str, str]:
values = {}
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.removeprefix("export ").strip()
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
value = value[1:-1]
elif " #" in value:
value = value.split(" #", 1)[0].rstrip()
values[key] = value
return values
def candidates() -> list[Path]:
paths: list[Path] = []
for start in (Path.cwd(), Path(__file__).resolve().parent):
for folder in (start, *start.parents):
if folder / ".env" not in paths:
paths.append(folder / ".env")
return paths
def load() -> Path | None:
if os.getenv("MINIFLOW_DOTENV", "1") == "0":
return None
for path in candidates():
if path.is_file():
for key, value in parse(path.read_text(encoding="utf-8")).items():
os.environ.setdefault(key, value)
LOADED.append(str(path))
return path
return None
+64 -17
View File
@@ -1,16 +1,21 @@
"""LLM providers: mock (default, no API key), ollama (local), openai, anthropic, gemini. """LLM providers: mock (default, no API key), openai (course default, key in .env), ollama (local), anthropic, gemini.
Select with the MINIFLOW_LLM environment variable. Select with MINIFLOW_LLM (usually set in .env). MINIFLOW_LLM_FALLBACK=ollama retries a failed call locally.
""" """
import json import json
import os import os
import sys
import time
import urllib.error import urllib.error
import urllib.request import urllib.request
from typing import Any from typing import Any
from miniflow import envfile
from miniflow.template import render from miniflow.template import render
envfile.load() # OPENAI_API_KEY and other settings from .env
class LLMError(RuntimeError): class LLMError(RuntimeError):
pass pass
@@ -111,26 +116,68 @@ def call_gemini(system: str, prompt: str, temperature: float) -> str:
return response.text or "" return response.text or ""
def call_openai(system: str, prompt: str, temperature: float) -> str: def call_openai(system: str, prompt: str, temperature: float, schema: dict | None = None,
max_tokens: int | None = None) -> str:
"""Call the OpenAI Responses API (OPENAI_MODEL, default gpt-4.1-mini).
When `schema` is given, the answer is JSON that follows it (structured output).
A model that does not accept temperature is called again without it.
"""
try: try:
from openai import OpenAI import openai
except ImportError as error: except ImportError as error:
raise LLMError("Install the extra first: uv sync --extra openai") from error raise LLMError("The openai package is missing. Run: uv sync") from error
if not os.getenv("OPENAI_API_KEY"): if not os.getenv("OPENAI_API_KEY"):
raise LLMError("OPENAI_API_KEY is not set") raise LLMError("OPENAI_API_KEY is not set (put it in the .env file)")
client = OpenAI() client = openai.OpenAI(timeout=float(os.getenv("OPENAI_TIMEOUT", "60")),
response = client.responses.create( max_retries=int(os.getenv("OPENAI_MAX_RETRIES", "2")))
model=os.getenv("OPENAI_MODEL", "gpt-4.1-mini"), model = os.getenv("OPENAI_MODEL", "gpt-4.1-mini")
instructions=system or None, request: dict[str, Any] = {"model": model, "input": prompt, "temperature": temperature}
input=prompt, if system:
temperature=temperature, request["instructions"] = system
) if max_tokens:
return response.output_text request["max_output_tokens"] = max(16, int(max_tokens)) # the API minimum is 16
if schema:
request["text"] = {"format": {"type": "json_schema", "name": "reply", "schema": schema, "strict": False}}
started = time.perf_counter()
try:
try:
response = client.responses.create(**request)
except openai.BadRequestError as error:
if "temperature" not in str(error):
raise
request.pop("temperature")
response = client.responses.create(**request)
except openai.AuthenticationError as error:
raise LLMError("OpenAI rejected the API key (check OPENAI_API_KEY in .env)") from error
except openai.APIConnectionError as error:
raise LLMError(f"Cannot reach the OpenAI API ({error})") from error
except openai.APIStatusError as error:
raise LLMError(f"OpenAI API returned HTTP {error.status_code}: {error.message}") from error
return (response.output_text or "").strip()
def complete(node: dict[str, Any], prompt: str, system: str, context: dict[str, Any]) -> tuple[str, str]: def complete(node: dict[str, Any], prompt: str, system: str, context: dict[str, Any]) -> tuple[str, str]:
"""Return (reply, provider_name).""" """Return (reply, provider_name).
provider = os.getenv("MINIFLOW_LLM", "mock").lower()
With MINIFLOW_LLM_FALLBACK set (for example ollama), a call that fails on the main provider
is tried once more there, and the reply says which provider answered.
"""
primary = os.getenv("MINIFLOW_LLM", "mock").lower()
try:
return _complete_once(node, prompt, system, context, primary)
except LLMError as error:
fallback = os.getenv("MINIFLOW_LLM_FALLBACK", "").strip().lower()
if not fallback or fallback == primary or type(error).__name__ == "BudgetError":
raise
print(f"[miniflow] {primary} failed ({error}); trying {fallback}", file=sys.stderr)
try:
return _complete_once(node, prompt, system, context, fallback)
except LLMError as second:
raise LLMError(f"{primary} failed: {error} / fallback {fallback} failed: {second}") from second
def _complete_once(node: dict[str, Any], prompt: str, system: str, context: dict[str, Any], provider: str) -> tuple[str, str]:
temperature = float(node.get("temperature", 0.2)) temperature = float(node.get("temperature", 0.2))
if provider == "mock": if provider == "mock":
return mock_reply(prompt, node.get("mock"), context), "mock" return mock_reply(prompt, node.get("mock"), context), "mock"
@@ -141,5 +188,5 @@ def complete(node: dict[str, Any], prompt: str, system: str, context: dict[str,
if provider == "gemini": if provider == "gemini":
return call_gemini(system, prompt, temperature), "gemini" return call_gemini(system, prompt, temperature), "gemini"
if provider == "openai": if provider == "openai":
return call_openai(system, prompt, temperature), "openai" return call_openai(system, prompt, temperature, node.get("json_schema")), "openai"
raise LLMError(f"Unknown MINIFLOW_LLM value: {provider} (use mock, ollama, openai, anthropic or gemini)") raise LLMError(f"Unknown MINIFLOW_LLM value: {provider} (use mock, ollama, openai, anthropic or gemini)")
@@ -0,0 +1,3 @@
import os
os.environ["MINIFLOW_DOTENV"] = "0" # tests never read the real .env (it may hold API keys)
@@ -0,0 +1,80 @@
"""OpenAI provider, .env loading and the Ollama fallback, without network access."""
import types
import pytest
from miniflow import envfile, llm
def test_parse_env_lines():
values = envfile.parse('# comment\nexport A=1\nB="two words"\nC=3 # note\n\nbroken line\n')
assert values == {"A": "1", "B": "two words", "C": "3"}
def test_load_keeps_shell_values(tmp_path, monkeypatch):
(tmp_path / ".env").write_text("MINIFLOW_TEST_A=from-file\nMINIFLOW_TEST_B=from-file\n", encoding="utf-8")
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("MINIFLOW_DOTENV", "1")
monkeypatch.setenv("MINIFLOW_TEST_A", "from-shell")
monkeypatch.delenv("MINIFLOW_TEST_B", raising=False)
assert envfile.load() == tmp_path / ".env"
assert llm.os.environ["MINIFLOW_TEST_A"] == "from-shell"
assert llm.os.environ["MINIFLOW_TEST_B"] == "from-file"
monkeypatch.delenv("MINIFLOW_TEST_B")
def test_openai_without_key(monkeypatch):
monkeypatch.setenv("MINIFLOW_LLM", "openai")
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("MINIFLOW_LLM_FALLBACK", raising=False)
with pytest.raises(llm.LLMError, match="OPENAI_API_KEY"):
llm.complete({}, "hello", "", {})
def test_openai_request_shape(monkeypatch):
sent = {}
class FakeResponses:
def create(self, **kwargs):
sent.update(kwargs)
return types.SimpleNamespace(output_text='{"ok": true}', usage=None)
class FakeClient:
def __init__(self, **_):
self.responses = FakeResponses()
import openai
monkeypatch.setattr(openai, "OpenAI", FakeClient)
monkeypatch.setenv("MINIFLOW_LLM", "openai")
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
monkeypatch.setenv("OPENAI_MODEL", "test-model")
schema = {"type": "object", "properties": {"ok": {"type": "boolean"}}}
reply, used = llm.complete({"temperature": 0, "json_schema": schema}, "hello", "be brief", {})
assert (reply, used) == ('{"ok": true}', "openai")
assert sent["model"] == "test-model" and sent["instructions"] == "be brief" and sent["temperature"] == 0
assert sent["text"]["format"]["schema"] == schema
def test_fallback_to_second_provider(monkeypatch, capsys):
monkeypatch.setenv("MINIFLOW_LLM", "openai")
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.setenv("MINIFLOW_LLM_FALLBACK", "ollama")
monkeypatch.setattr(llm, "call_ollama", lambda *args, **kwargs: "from ollama")
reply, used = llm.complete({}, "hello", "", {})
assert (reply, used) == ("from ollama", "ollama")
assert "openai failed" in capsys.readouterr().err
def test_both_providers_fail(monkeypatch):
monkeypatch.setenv("MINIFLOW_LLM", "openai")
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.setenv("MINIFLOW_LLM_FALLBACK", "ollama")
def broken(*args, **kwargs):
raise llm.LLMError("Cannot reach Ollama")
monkeypatch.setattr(llm, "call_ollama", broken)
with pytest.raises(llm.LLMError, match="OPENAI_API_KEY.*fallback ollama failed"):
llm.complete({}, "hello", "", {})
+4
View File
@@ -647,6 +647,9 @@ wheels = [
name = "miniflow" name = "miniflow"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [
{ name = "openai" },
]
[package.optional-dependencies] [package.optional-dependencies]
anthropic = [ anthropic = [
@@ -668,6 +671,7 @@ dev = [
requires-dist = [ requires-dist = [
{ name = "anthropic", marker = "extra == 'anthropic'", specifier = ">=1.13" }, { name = "anthropic", marker = "extra == 'anthropic'", specifier = ">=1.13" },
{ name = "google-genai", marker = "extra == 'gemini'", specifier = ">=1.0" }, { name = "google-genai", marker = "extra == 'gemini'", specifier = ">=1.0" },
{ name = "openai", specifier = ">=1.40" },
{ name = "openai", marker = "extra == 'openai'", specifier = ">=1.40" }, { name = "openai", marker = "extra == 'openai'", specifier = ">=1.40" },
] ]
provides-extras = ["gemini", "openai", "anthropic"] provides-extras = ["gemini", "openai", "anthropic"]
Binary file not shown.
+22 -17
View File
@@ -6,7 +6,7 @@
```mermaid ```mermaid
flowchart LR flowchart LR
A["1. 실습 환경<br/>WSL · uv · Ollama"] --> B["2. 확인 명령<br/>pytest · check"] A["1. 실습 환경<br/>WSL · uv · API 키 · Ollama"] --> B["2. 확인 명령<br/>pytest · check"]
B --> C["3. 미리 읽기<br/>JSON · 지난 실습"] B --> C["3. 미리 읽기<br/>JSON · 지난 실습"]
C --> D["4. 생각해 오기<br/>내 업무 시나리오"] C --> D["4. 생각해 오기<br/>내 업무 시나리오"]
``` ```
@@ -15,19 +15,20 @@ flowchart LR
## 1. 실습 환경 준비 (전날 필수, 약 40분) ## 1. 실습 환경 준비 (전날 필수, 약 40분)
오늘 실습은 Windows 11 노트북의 **WSL(Ubuntu)** 에서, LLM은 노트북에서 직접 실행하는 **Ollama**로 진행합니다. API 키는 필요 없습니다. 오늘 실습은 Windows 11 노트북의 **WSL(Ubuntu)** 에서 진행합니다. LLM은 과정에서 받은 키로 **OpenAI API**(`gpt-4.1-mini`)를 쓰고, 키·인터넷에 문제가 있으면 노트북에서 직접 실행하는 **Ollama**(`gemma3:4b`)가 대신 답합니다. 이 구성은 65일차 이후에도 그대로 씁니다.
자세한 순서는 **[사전 준비 안내 (SETUP-WSL-OLLAMA)](../../Last3Mon/64/SETUP-WSL-OLLAMA.md)** 를 따라 하세요. 할 일만 요약하면 다음과 같습니다. 자세한 순서는 **[사전 준비 안내 (SETUP-WSL-LLM)](../../Last3Mon/64/SETUP-WSL-LLM.md)** 를 따라 하세요. 할 일만 요약하면 다음과 같습니다.
| 순서 | 할 일 | 시간 | | 순서 | 할 일 | 시간 |
| --- | --- | --- | | --- | --- | --- |
| 1 | WSL(Ubuntu) 동작 확인 | 5분 | | 1 | WSL(Ubuntu) 동작 확인 | 5분 |
| 2 | 노트북 메모리 확인 → 사용할 모델 결정 (8GB: `gemma3:1b`, 16GB 이상: `gemma3:4b`) | 2분 | | 2 | uv 설치, 수업 자료 받기 (WSL 홈 `~` 아래) | 10분 |
| 3 | uv 설치, 수업 저장소 받기 (WSL 홈 `~` 아래) | 10분 | | 3 | `Last3Mon/.env`에 OpenAI API 키 넣기 | 3분 |
| 4 | Ollama 설치와 모델 다운로드 | 10~20분 | | 4 | 노트북 메모리 확인 → 대체 모델 결정 (8GB: `gemma3:1b`, 16GB 이상: `gemma3:4b`) | 2분 |
| 5 | `~/.bashrc`에 `MINIFLOW_LLM`, `OLLAMA_MODEL` 설정 | 3분 | | 5 | Ollama 설치와 모델 다운로드 (대체용) | 10~20분 |
| 6 | 연결 확인 (OpenAI, 대체 Ollama) | 5분 |
> 모델 파일이 1~3GB라서 수업 중에 여러 명이 동시에 받으면 강의실 네트워크가 매우 느려집니다. **다운로드는 꼭 집에서 미리** 해 주세요. > Ollama 모델 파일이 1~3GB라서 수업 중에 여러 명이 동시에 받으면 강의실 네트워크가 매우 느려집니다. **다운로드는 꼭 집에서 미리** 해 주세요.
## 2. 준비가 끝났는지 확인 (전날 필수, 5분) ## 2. 준비가 끝났는지 확인 (전날 필수, 5분)
@@ -35,17 +36,19 @@ WSL 터미널에서 실습 폴더로 이동해 아래 두 명령을 실행합니
```bash ```bash
cd ~/<수업 저장소>/Last3Mon/64/workflow-lab cd ~/<수업 저장소>/Last3Mon/64/workflow-lab
uv run pytest -q # 12 passed, 1 skipped 가 나오면 성공 uv run pytest -q # 18 passed, 1 skipped 가 나오면 성공
uv run miniflow check # Reply (ollama, ...) 가 나오면 성공 uv run miniflow check # Reply (openai, ...) 가 나오면 성공
OPENAI_API_KEY=sk-wrong uv run miniflow check # Reply (ollama, ...) 가 나오면 대체도 준비 완료
``` ```
| 결과 | 의미 | | 결과 | 의미 |
| --- | --- | | --- | --- |
| 두 명령 모두 성공 | 준비 완료 | | 세 명령 모두 성공 | 준비 완료 |
| `pytest`는 성공, `check`가 실패 | Ollama 설정 문제. 안내서 7절(문제 해결) 확인 | | `pytest`는 성공, 첫 `check`가 실패 | `.env`의 키 문제. 안내서 8절(문제 해결) 확인 |
| 첫 `check`는 성공, 두 번째가 실패 | 대체용 Ollama 문제. 수업은 OpenAI로 할 수 있으니 1교시 전에 말씀해 주세요 |
| 둘 다 실패 | uv 또는 저장소 위치 문제. 안내서 3절부터 다시 확인 | | 둘 다 실패 | uv 또는 저장소 위치 문제. 안내서 3절부터 다시 확인 |
해결이 안 되면 수업 당일 1교시 전에 말씀해 주세요. 그동안은 `export MINIFLOW_LLM=mock`으로 Mock LLM을 써서 실습을 따라올 수 있습니다. 해결이 안 되면 수업 당일 1교시 전에 말씀해 주세요. 키를 아직 못 받았으면 `.env`의 `MINIFLOW_LLM=ollama`로, 둘 다 안 되면 `export MINIFLOW_LLM=mock`으로 Mock LLM을 써서 실습을 따라올 수 있습니다.
--- ---
@@ -118,14 +121,16 @@ uv run miniflow check # Reply (ollama, ...) 가 나오면 성공
## 5. 당일 준비물 ## 5. 당일 준비물
- 충전된 노트북과 충전기 (Ollama는 배터리를 많이 사용합니다) - 충전된 노트북과 충전기 (대체용 Ollama는 배터리를 많이 사용합니다)
- Windows Terminal (WSL 터미널), VS Code - Windows Terminal (WSL 터미널), VS Code
- 웹 브라우저 (워크플로우 그림 확인용 [Mermaid Live Editor](https://mermaid.live)) - 웹 브라우저 (워크플로우 그림 확인용 [Mermaid Live Editor](https://mermaid.live))
- (선택) OpenAI/Anthropic/Gemini 중 가지고 있는 API 키 하나 (예: 10일차에 발급한 Gemini 키) — 로컬 모델과 클라우드 모델을 비교해 볼 수 있습니다 - 과정에서 받은 OpenAI API 키 (`Last3Mon/.env`에 넣어 오기). 키는 채팅·문서·화면 캡처에 올리지 않습니다
- (선택) Anthropic·Gemini 키가 있으면 (예: 10일차에 발급한 Gemini 키) 같은 워크플로우를 다른 제공사 모델로도 비교해 볼 수 있습니다
## 확인 ## 확인
- [ ] `uv run pytest -q` → `12 passed, 1 skipped` - [ ] `uv run pytest -q` → `18 passed, 1 skipped`
- [ ] `uv run miniflow check` → `Reply (ollama, ...)` - [ ] `uv run miniflow check` → `Reply (openai, ...)`
- [ ] `OPENAI_API_KEY=sk-wrong uv run miniflow check` → `Reply (ollama, ...)`
- [ ] JSON 기초(3.1)를 읽었다 - [ ] JSON 기초(3.1)를 읽었다
- [ ] Lab 03에서 분석할 업무 후보를 하나 생각했다 - [ ] Lab 03에서 분석할 업무 후보를 하나 생각했다