D-2 수정 작업: OpenAI API 우선 없으면 Ollama 적용
This commit is contained in:
1 parent
5eedbdd039
commit
2fe0dc8de1
16 files changed
+466
-218
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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의 결과·속도 차이를 표로 정리했다.
|
||||||
@@ -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` 결과를 분석 문서의 "구현한 워크플로우" 항목에 붙여 넣습니다.
|
||||||
|
|
||||||
|
|||||||
@@ -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, ...) : 대체 경로
|
||||||
```
|
```
|
||||||
@@ -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, ...)`가 나온다
|
||||||
@@ -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, ...)`가 나온다
|
|
||||||
@@ -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 # 연결 확인
|
|
||||||
```
|
```
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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", "", {})
|
||||||
Generated
+4
@@ -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
@@ -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에서 분석할 업무 후보를 하나 생각했다
|
||||||
Reference in new issue
Block a user