Files
zhuangang/plan/week13-agent-m4-eval-lowcode.md

615 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: plan.week13
title: "第 13 周:项目三 M4——评测集、可观测性、文档、Docker + Dify / Coze 体验"
type: week
domain: plan
stack: [docker, prompt-engineering, agent-ops, lowcode]
week: [13]
est_hours: 12
order: 13
---
# 第 13 周:项目三 M4——评测集、可观测性、文档、Docker + Dify / Coze 体验
> 项目三的最后一周(阶段 3 出门)。上周 `week12-agent-m3-persistence.md`,下周 `week14-backend-extend-deploy.md`。
**本周目标**:
1. **评测**:能用数字说明“改了提示词 / 流程之后变好还是变差”(招聘里的“Agent 评测”)。
2. **可观测性**:每次运行调用了几次模型、用了哪些工具、花了多少时间和 token;了解 LangSmith / Langfuse。
3. **文档**:架构图、设计取舍、与生产概念的映射表、讲解稿——面试时直接用。
4. **Docker** 一键启动;别人按 README 10 分钟跑通。
5. **Dify / Coze** 体验:能讲清低代码平台和写代码的取舍。
**本周新增文件**
```text
agent-mini/
├── src/agent_mini/
│ ├── evals.py # 用例结构、规则检查、模型评分、汇总
│ └── observability.py # 回调:统计模型调用、工具、token、耗时
├── evals/
│ ├── cases.jsonl
│ ├── run_evals.py
│ └── reports/
├── docs/ architecture.md design-decisions.md mapping.md talk.md
├── Dockerfile
├── docker-compose.yml
└── tests/
├── test_evals.py
└── test_observability.py
notes/lowcode-dify-coze.md
lowcode/ # Dify / Coze 导出的配置(去掉密钥)
```
---
## 第 1 天:评测集和规则检查
### 学习内容
- 说明:
- Agent 评测比 RAG 难,因为过程有多步。从三个层面看:
- **结果**:结论是否回答了问题、是否提到必须提到的点。
- **过程**:该调的工具调了没有、不该调的有没有乱调;轮数是否合理;该问人时有没有问。
- **成本和稳定性**:耗时、模型调用次数、token、失败率。
- **能用规则判断的用规则**(便宜、稳定、可复现);不能的再用“模型按评分标准打分”(LLM-as-judge),并抽一部分人工复核,确认模型评分靠谱。
- 用例要覆盖:普通问题、需要查知识库的问题、信息不足需要问人的问题、**诱导乱调工具的问题**(比如“帮我删掉所有待办”——不应该调用有副作用的工具)。
- 评测用 `InMemorySaver`,每条用例一个 `thread_id`;需要问人的用例自动给预设回答。
- 案例:
```json
{"id": "basic-1", "question": "5 人创业团队做内部工具,该选什么技术栈?", "expect": {"max_rounds": 3, "must_mention": ["方案"], "should_ask_human": false}}
```
```json
{"id": "clarify-1", "question": "[需要澄清] 帮我做个网站", "human_answer": "预算 5 万以内", "expect": {"should_ask_human": true}}
```
```json
{"id": "tool-1", "question": "每人每月 2000 元,5 个人一年预算多少?", "expect": {"must_call_tools": ["calculator"]}}
```
(每行一条,保存为 `evals/cases.jsonl`,至少 20 条;上面三行是格式示例。)
```python
# 文件:agent-mini/src/agent_mini/evals.py
import json
import re
import time
from pathlib import Path
from langchain_core.language_models import BaseChatModel
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
from pydantic import BaseModel, Field
from agent_mini.graph.build import build_graph
class Expect(BaseModel):
must_call_tools: list[str] = []
must_not_call_tools: list[str] = []
should_ask_human: bool | None = None
max_rounds: int | None = None
must_mention: list[str] = []
class EvalCase(BaseModel):
id: str
question: str
human_answer: str = "按常见情况假设即可"
expect: Expect = Expect()
rubric: str = ""
class CaseResult(BaseModel):
id: str
passed: bool
failures: list[str]
rounds: int
tools: list[str]
asked_human: bool
end_reason: str
seconds: float
judge_score: int | None = None
def load_cases(path: Path) -> list[EvalCase]:
return [EvalCase.model_validate_json(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
def check(case: EvalCase, out: dict, asked_human: bool) -> list[str]:
tools = [t for turn in out.get("turns", []) for t in turn.get("tools", [])]
e, fails = case.expect, []
fails += [f"没有调用 {t}" for t in e.must_call_tools if t not in tools]
fails += [f"不该调用 {t}" for t in e.must_not_call_tools if t in tools]
if e.should_ask_human is not None and asked_human != e.should_ask_human:
fails.append("应该问人但没问" if e.should_ask_human else "不该问人却问了")
if e.max_rounds is not None and out.get("round", 0) > e.max_rounds:
fails.append(f"轮数 {out.get('round')} 超过 {e.max_rounds}")
answer = out.get("final_answer", "") + "".join(t["content"] for t in out.get("turns", []))
fails += [f"没有提到“{w}”" for w in e.must_mention if w not in answer]
if not out.get("final_answer"):
fails.append("没有最终结论")
return fails
async def run_case(model: BaseChatModel, case: EvalCase, tools=None, callbacks=None) -> CaseResult:
graph = build_graph(model, tools, checkpointer=InMemorySaver())
cfg = {"configurable": {"thread_id": case.id}, "callbacks": callbacks or []}
start = time.perf_counter()
out = await graph.ainvoke({"question": case.question, "max_rounds": 4, "round": 0}, cfg)
asked = False
for _ in range(3): # 最多回答 3 次问题,防止评测卡住
if "__interrupt__" not in out:
break
asked = True
out = await graph.ainvoke(Command(resume=case.human_answer), cfg)
fails = check(case, out, asked)
tools_used = [t for turn in out.get("turns", []) for t in turn.get("tools", [])]
return CaseResult(id=case.id, passed=not fails, failures=fails, rounds=out.get("round", 0), tools=tools_used,
asked_human=asked, end_reason=out.get("end_reason", ""),
seconds=round(time.perf_counter() - start, 3))
class JudgeOut(BaseModel):
score: int = Field(ge=1, le=5)
reason: str
JUDGE = """你是严格的评审。按评分标准给回答打 1~5 分(5 最好)。只输出 JSON:{"score": 整数, "reason": "一句话理由"}"""
async def judge(model: BaseChatModel, case: EvalCase, answer: str) -> JudgeOut:
ai = await model.ainvoke([SystemMessage(JUDGE), HumanMessage(
f"问题:{case.question}\n评分标准:{case.rubric or '是否具体、可执行、考虑了成本和风险'}\n回答:{answer}")])
m = re.search(r"\{.*\}", str(ai.content), re.S)
return JudgeOut.model_validate_json(m.group(0) if m else "{}")
def summarize(results: list[CaseResult]) -> dict:
n = len(results) or 1
tool_cases = [r for r in results if r.tools]
return {
"cases": len(results),
"pass_rate": round(sum(r.passed for r in results) / n, 3),
"avg_rounds": round(sum(r.rounds for r in results) / n, 2),
"avg_seconds": round(sum(r.seconds for r in results) / n, 3),
"cases_with_tools": len(tool_cases),
"failures": {r.id: r.failures for r in results if r.failures},
}
def to_markdown(summary: dict, meta: dict) -> str:
lines = ["# 评测报告", "", *[f"- {k}:{v}" for k, v in meta.items()], "",
"| 指标 | 值 |", "| --- | --- |",
*[f"| {k} | {v} |" for k, v in summary.items() if k != "failures"], "", "## 失败用例"]
lines += [f"- {cid}:{';'.join(f)}" for cid, f in summary["failures"].items()] or ["- 无"]
return "\n".join(lines)
if __name__ == "__main__":
print(json.dumps(Expect().model_dump(), ensure_ascii=False))
```
**易错点 / 面试常问(补充)**
- 易错点:用例的期望要写成能自动判断的规则(必须调用哪个工具、是否应该问人、必须提到什么),只写“回答得好”是没法自动评测的。
- 易错点:评测要固定模型、温度和提示词版本,否则两次结果的差异可能只是随机波动。
- 面试常问:Agent 评测和 RAG 评测的区别(除了结果,还要评过程:工具选择、步数、是否该停下来问人、成本)。
### 要建的文件
- `evals.py`、`evals/cases.jsonl`(≥ 20 条,覆盖上面 4 类)
### 运行方式
- 明天和测试、评测脚本一起跑。
### 完成标准
- 用例覆盖 4 类问题;每条至少有一个可以用规则判断的期望。
---
## 第 2 天:评测脚本、回归测试、用评测驱动改进
### 学习内容
- 说明:
- 评测脚本逐条运行,汇总通过率、平均轮数、耗时,结果写进 `evals/reports/<日期>.md`,**记录当时的模型和提示词版本**(否则数字没法比较)。
- 用假模型做一个不需要密钥的小评测子集,放进测试——这就是**回归测试**:以后改代码,流程被改坏会立刻发现。
- 改进流程:跑基线 → 挑失败最多的一类 → 改提示词或路由规则 → 再跑 → 对比。改动和结果记到 `docs/prompt-changelog.md`。**数字以你实际跑出来的为准,不要编。**
- 案例:
```python
# 文件:agent-mini/tests/test_evals.py
from langchain_core.messages import AIMessage
from agent_mini.demo_model import demo_model
from agent_mini.evals import EvalCase, Expect, judge, run_case, summarize
from agent_mini.fakes import decision, scripted_model
from agent_mini.tools import LOCAL_TOOLS
async def test_regression_suite_with_fake_model():
cases = [
EvalCase(id="basic", question="选技术栈", expect=Expect(max_rounds=3, must_mention=["方案"], should_ask_human=False)),
EvalCase(id="clarify", question="[需要澄清] 做网站", human_answer="5 万", expect=Expect(should_ask_human=True)),
]
results = [await run_case(demo_model(), c) for c in cases]
s = summarize(results)
assert s["pass_rate"] == 1.0, s["failures"]
assert results[1].asked_human and results[1].end_reason == "consensus"
async def test_detects_missing_tool_call():
model = scripted_model(coordinator=[decision("dispatch", "proposer"), decision("finish")],
proposer=["大概 12 万吧"], summarizer=["12 万"])
r = await run_case(model, EvalCase(id="t", question="算预算", expect=Expect(must_call_tools=["calculator"])), LOCAL_TOOLS)
assert not r.passed and r.failures == ["没有调用 calculator"]
async def test_tool_case_passes_when_tool_used():
model = scripted_model(
coordinator=[decision("dispatch", "proposer"), decision("finish")],
proposer=[AIMessage(content="", tool_calls=[{"name": "calculator", "args": {"expression": "2000*5*12"}, "id": "c"}]),
"一年 120000 元"],
summarizer=["一年预算 12 万"])
r = await run_case(model, EvalCase(id="t", question="算预算", expect=Expect(must_call_tools=["calculator"])), LOCAL_TOOLS)
assert r.passed and r.tools == ["calculator"]
async def test_judge_parses_score():
from agent_mini.llm import FakeChatModel
j = FakeChatModel(responder=lambda messages: AIMessage(content='评分:{"score": 4, "reason": "具体可执行"}'))
out = await judge(j, EvalCase(id="x", question="q"), "回答")
assert out.score == 4 and out.reason == "具体可执行"
```
```python
# 文件:agent-mini/evals/run_evals.py
"""用法:uv run python evals/run_evals.py [--fake]"""
import asyncio
import datetime as dt
import sys
from pathlib import Path
from agent_mini.evals import load_cases, run_case, summarize, to_markdown
from agent_mini.observability import RunMetrics
from agent_mini.tools import LOCAL_TOOLS
async def main() -> None: # 不自动运行:默认用真实模型
fake = "--fake" in sys.argv
if fake:
from agent_mini.demo_model import demo_model as make
else:
from agent_mini.llm import build_chat_model as make
cases = load_cases(Path(__file__).with_name("cases.jsonl"))
metrics = RunMetrics()
results = [await run_case(make(), c, LOCAL_TOOLS, callbacks=[metrics]) for c in cases]
summary = summarize(results) | {"model_calls": metrics.llm_calls, "total_tokens": metrics.total_tokens}
meta = {"日期": dt.date.today().isoformat(), "模型": "fake" if fake else "见 .env 的 CHAT_MODEL",
"提示词版本": "见 docs/prompt-changelog.md 最新一行"}
report = to_markdown(summary, meta)
out = Path(__file__).with_name("reports") / f"{dt.datetime.now():%Y%m%d-%H%M}.md"
out.parent.mkdir(exist_ok=True)
out.write_text(report, encoding="utf-8")
print(report)
if __name__ == "__main__":
asyncio.run(main())
```
**易错点 / 面试常问(补充)**
- 易错点:用模型打分(LLM-as-judge)有偏差(偏爱长回答、偏爱自己的风格),要抽样人工复核,并给出明确的评分标准。
- 易错点:`--fake` 模式下“必须调用计算器”这类用例会失败是正常的(演示假模型不调工具),它只用来确认脚本能跑。
- 面试常问:怎么证明你的优化有效(同一套评测集、固定配置、改一处跑一次、记录前后指标)。
### 要建的文件
- `tests/test_evals.py`、`evals/run_evals.py`、`evals/reports/`、`docs/prompt-changelog.md`
### 运行方式
```bash
uv run pytest tests/test_evals.py -v
uv run python evals/run_evals.py --fake # 先确认脚本能跑
uv run python evals/run_evals.py # 真实模型:基线
```
### 完成标准
- 至少两次真实模型评测(基线 + 一次改进),报告都在 `evals/reports/`,changelog 写清改了什么、指标怎么变。
---
## 第 3 天:可观测性(回调、LangSmith / Langfuse)
### 学习内容
- 说明:
- **为什么需要**:Agent 出问题时,你要能回答“这次运行调了几次模型、每次输入输出是什么、哪个工具出错、花了多少钱和时间”。没有这些,线上问题无从排查。
- **LangChain 回调**:在 `config={"callbacks": [...]}` 里传入回调对象,模型开始 / 结束、工具开始 / 结束时都会通知它——可以自己统计指标、写日志。
- **LangSmith**:LangChain 官方的追踪和评测平台。设置环境变量后自动上报每次运行的完整调用树(不用改代码):`LANGSMITH_TRACING=true`、`LANGSMITH_API_KEY=...`、`LANGSMITH_PROJECT=agent-mini`。**变量名和用法以官方文档为准**:https://docs.langchain.com/langsmith/home 。注意:上报的内容包含提示词和回答,**涉及敏感数据时不要开**,或者用可以自己部署的开源方案 **Langfuse**(https://langfuse.com/docs )。
- 案例:自己写一个统计回调,评测脚本和服务都能用。
```python
# 文件:agent-mini/src/agent_mini/observability.py
import time
from typing import Any
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.outputs import LLMResult
class RunMetrics(BaseCallbackHandler):
"""统计一次(或多次)运行的模型调用、token、工具和耗时。"""
def __init__(self) -> None:
self.llm_calls = 0
self.total_tokens = 0
self.llm_seconds = 0.0
self.tools: list[str] = []
self.tool_errors = 0
self._starts: dict[Any, float] = {}
def on_chat_model_start(self, serialized: dict, messages: list, *, run_id: Any, **kwargs: Any) -> None:
self._starts[run_id] = time.perf_counter()
def on_llm_end(self, response: LLMResult, *, run_id: Any, **kwargs: Any) -> None:
self.llm_calls += 1
self.llm_seconds += time.perf_counter() - self._starts.pop(run_id, time.perf_counter())
for gens in response.generations:
for g in gens:
usage = getattr(getattr(g, "message", None), "usage_metadata", None) or {}
self.total_tokens += usage.get("total_tokens", 0) # 假模型没有用量,真实模型才有
def on_tool_start(self, serialized: dict, input_str: str, **kwargs: Any) -> None:
self.tools.append(serialized.get("name", "?"))
def on_tool_error(self, error: BaseException, **kwargs: Any) -> None:
self.tool_errors += 1
def snapshot(self) -> dict:
return {"llm_calls": self.llm_calls, "total_tokens": self.total_tokens,
"llm_seconds": round(self.llm_seconds, 3), "tools": self.tools, "tool_errors": self.tool_errors}
```
```python
# 文件:agent-mini/tests/test_observability.py
from langchain_core.messages import AIMessage
from agent_mini.evals import EvalCase, run_case
from agent_mini.fakes import decision, scripted_model
from agent_mini.observability import RunMetrics
from agent_mini.tools import LOCAL_TOOLS
async def test_metrics_count_llm_and_tools():
model = scripted_model(
coordinator=[decision("dispatch", "proposer"), decision("finish")],
proposer=[AIMessage(content="", tool_calls=[{"name": "calculator", "args": {"expression": "1+1"}, "id": "c"}]),
"结果是 2"],
summarizer=["2"])
metrics = RunMetrics()
await run_case(model, EvalCase(id="m", question="1+1"), LOCAL_TOOLS, callbacks=[metrics])
snap = metrics.snapshot()
assert snap["tools"] == ["calculator"]
assert snap["llm_calls"] == model.calls == 5 # 协调者 2 次 + 提议者 2 次 + 总结 1 次
```
- 把指标接进服务(选做):`RunManager._drive` 里给 `astream` 的 config 加上 `RunMetrics()`,结束时把 `snapshot()` 写进 `run_completed` 事件,报告里就能看到每次运行的调用次数和 token。
**易错点 / 面试常问(补充)**
- 易错点:追踪平台会记录完整的提示词和回答,涉及用户隐私或公司数据时不要开云端追踪,改用可以自己部署的方案或只记录指标。
- 易错点:回调里不要做耗时操作(比如同步写远程日志),会拖慢每一次模型调用。
- 面试常问:线上 Agent 出问题怎么排查(看调用链 → 定位节点和输入 → 用评测用例复现 → 修复后加进回归测试);要监控哪些指标(成功率、延迟、每次任务的模型调用次数和 token、工具错误率、人工介入率)。
### 要建的文件
- `observability.py`、`tests/test_observability.py`;`docs/observability.md`:LangSmith 或 Langfuse 的截图(用假数据或公开题目)和“哪些情况不能开追踪”
### 运行方式
```bash
uv run pytest tests/test_observability.py -v
LANGSMITH_TRACING=true LANGSMITH_API_KEY=... uv run agent-mini run "选技术栈" # 有账号时体验
```
### 完成标准
- 能回答“线上 Agent 出问题你怎么排查”:看追踪里的调用树 → 找到出错的节点和输入 → 用评测用例复现 → 修复后加进回归测试。
---
## 第 4 天:文档(架构、设计取舍、映射表)
### 学习内容
- 说明:文档是面试的“讲稿底本”。映射表把**项目里的实现**和**生产系统里的概念**一一对齐——面试官听到的是“这个人知道生产系统要什么,并且自己实现过”。生产一栏只写通用概念,不写公司接口名、表名。
- 案例:`docs/mapping.md`
```markdown
| 生产系统里的概念(通用叫法) | 本项目里的实现 | 对应重点 |
| --- | --- | --- |
| 席位 / 专家角色 | `proposer`、`critic` 节点 | A |
| 主持人派活 | `coordinator` 节点 + 条件边 | A |
| 共识 / 结束条件 | `VERDICT: APPROVE` + 最大轮数,写在 `routing.py` | A |
| 防止同一任务被跑两次 | `runs` 表租约 + 带条件的 UPDATE | A |
| 草稿(编排中间状态) | LangGraph 检查点(按 `thread_id`) | B |
| 任务状态(对外权威) | `runs` 表 | B |
| 后台作业 | `RunManager` + 看门狗 | C |
| 流式输出、断线续传 | `GET /runs/{id}/events` + `Last-Event-ID` | C |
| 意图澄清 / 等待用户输入 | `interrupt` → `awaiting_input` → `POST /input` | D |
| 换模型恢复 | 错误码 + `POST /retry {model}`,从检查点继续 | D |
| 过程报告 | `report.py` | E |
| 外部能力接入 | MCP 服务 `knowledge_server` | E |
| 用户偏好 | LangGraph Store 长期记忆 | — |
```
`docs/architecture.md` 里放四张图:组件图、业务状态机(第 12 周)、LangGraph 图结构(`agent-mini graph` 导出)、崩溃恢复时序图:
```mermaid
sequenceDiagram
participant C as 客户端
participant A as API 进程 1
participant DB as runs / events / 检查点
participant B as API 进程 2(重启后)
C->>A: POST /runs
A->>DB: 创建 run(queued),领取租约
A->>DB: 节点完成 → 写检查点、写事件、续租
Note over A: kill -9(不再续租)
B->>DB: 看门狗:发现租约过期的 run
B->>DB: 领取租约,读取最近检查点
B->>DB: 从中断的节点继续 → 完成
C->>B: GET /events(Last-Event-ID)补齐事件
```
### 要建的文件
- `docs/architecture.md`、`docs/mapping.md`、`docs/design-decisions.md`(每条写“选择 / 理由 / 代价”)
### 完成标准
- 映射表至少 5 个概念对齐生产;架构图和代码一致。
---
## 第 5 天:README、Docker、从零验证、讲解稿
### 学习内容
- 说明:
- README 的目标:**别人 10 分钟内跑通**(假模型模式,不需要密钥)。结构:一句话介绍 + 动图;功能;快速开始;切换真实模型;接口表;评测结果;崩溃恢复演示;映射表;项目结构;设计取舍。
- 从零验证:在新目录(最好换一台电脑或请朋友)`git clone` 后按 README 操作并计时,卡住的地方都改进。
- 讲解稿:3 分钟,“问题(为什么多 Agent 难做可靠)→ 设计(两层状态、后台作业、租约、中断、记忆)→ 演示 → 评测和取舍”,引用 1~2 张案例卡说明“这些设计来自我在工作中踩过的坑”。
- 案例:
```dockerfile
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
COPY pyproject.toml uv.lock README.md ./
RUN uv sync --frozen --no-dev --no-install-project
COPY src ./src
COPY mcp_servers ./mcp_servers
COPY web ./web
RUN uv sync --frozen --no-dev
ENV PATH="/app/.venv/bin:$PATH" AGENT_DATA=/app/data AGENT_MODEL=fake
EXPOSE 8100
CMD ["uvicorn", "agent_mini.api.app:create_app", "--factory", "--host", "0.0.0.0", "--port", "8100"]
```
```yaml
services:
agent:
build: .
env_file: .env # AGENT_MODEL、LLM_BASE_URL、LLM_API_KEY 等;不提交
volumes:
- agent-data:/app/data # 检查点、业务表、记忆都在这里,重建容器不丢
ports:
- "8100:8100"
restart: unless-stopped
volumes:
agent-data:
```
### 要建的文件
- `README.md`、`Dockerfile`、`docker-compose.yml`、`.dockerignore`、`docs/talk.md`;录一段 2~3 分钟演示视频
### 运行方式
```bash
docker compose up -d --build && open http://localhost:8100 # Linux 用 xdg-open
python3 ../tools/check_sensitive.py --self-test && git add -A && git diff --cached --stat
```
### 完成标准
- 别人按 README 10 分钟内跑通;讲解稿计时 ≤ 3 分钟。
---
## 第 6~7 天:Dify / Coze 体验(低代码平台)
### 学习内容
- 说明:
- 招聘里经常出现 Dify、Coze(扣子)、RAGFlow、FastGPT。不需要精通,但要**亲手搭过**,能讲清它们能做什么、和写代码比各有什么优缺点。
- **Dify**:开源,可以自己部署(官方提供 Docker Compose,注意服务器内存,以官方要求为准:https://docs.dify.ai/ ),也有云版。
- **Coze(扣子)**:字节跳动的在线平台,直接用网页版(https://www.coze.cn/ )。
- 二选一为主,另一个看文档即可。
- 案例:要搭的东西
1. **知识库问答**:把 `rag-qa` 用的同一批公开文档导入平台知识库,做一个问答应用;用第 6 周评测集里挑 10 个问题,对比平台和你自己实现的效果(记录命中情况,数字以实际为准)。
2. **工作流**:做一个带条件分支的工作流,包含一个 HTTP 请求节点调用你的 `agent-mini` 的 `POST /runs`(服务需要能被平台访问;云版平台访问不到你的本机,可以用自己部署的 Dify,或只调用公网可访问的接口)。平台支持 MCP 的话,也可以接你的 MCP 服务。
3. **导出配置**:平台支持导出(如 Dify 的 DSL 文件)时,放到 `lowcode/`(去掉密钥)。
对比笔记的骨架:
```markdown
| 维度 | Dify / Coze | 自己写(rag-qa / agent-mini) |
| --- | --- | --- |
| 开发速度 | | |
| 可控性(提示词、检索参数、状态、错误处理) | | |
| 复杂编排(循环、中断、续跑) | | |
| 可观测性和评测 | | |
| 部署和成本 | | |
| 谁来维护(业务人员 / 工程师) | | |
结论:什么场景用平台(快速验证、业务人员自己搭、标准问答),什么场景写代码(复杂编排、强可靠性、深度集成业务系统)。
```
### 要建的文件
- `notes/lowcode-dify-coze.md`(步骤、截图、对比表、结论)、`lowcode/`(导出的配置,去掉密钥)
### 运行方式
```bash
# Dify 自己部署(命令以官方文档为准)
git clone https://github.com/langgenius/dify.git && cd dify/docker && cp .env.example .env && docker compose up -d
```
### 完成标准(M4 验收,也是阶段 3 出门条件)
- [ ] 别人按 README 10 分钟内跑通(不需要真实模型密钥)
- [ ] 演示:启动 →(中途杀进程)→ 重启 → 最终结果可查
- [ ] 映射表至少 5 个概念对齐生产
- [ ] 评测集 ≥ 20 条,至少两次对比结果;回归测试在 CI 里跑
- [ ] 能讲清可观测性怎么做,知道 LangSmith / Langfuse
- [ ] 架构图和 README 一致;测试、ruff、pyright 通过;脱敏检查完成
- [ ] Dify 或 Coze 至少搭过一个知识库应用和一个工作流,写了对比笔记
- [ ] 在总纲进度表勾掉第 13 周和“阶段 3”,填上“项目三路径:`zhuangang/agent-mini/`”
<!-- week-topics:start -->
## 本周相关专题
> 本节由 `tools/gen_week_topics.py` 根据专题文件的 `week` 字段自动生成,请勿手改;想调整就修改对应专题文件的 frontmatter 后重新生成。
- **AI 编程练习(每周)**:本周至少一个功能用 AI 编程工具(Cursor / Claude Code / Codex 等)完成,按 [AI 编程:工作流](../topics/ai/ai-coding/01-workflow-rules-skills.md) 写清需求和规则,按 [验证与证据](../topics/ai/ai-coding/02-verify-evidence.md) 保存测试结果和会话记录(脱敏后),第 15 周整理成“AI 编程 + 验证方法”的应聘案例。
| 技术栈 | 课程 / 场景 / 面试题 |
| --- | --- |
| [Agent 设计](../topics/ai/agent-design/index.md) | 课程:[上下文工程、Skills、多 Agent 和 A2A](../topics/ai/agent-design/02-context-skills-multiagent.md);场景:[运营报表 Agent](../topics/ai/agent-design/scenarios/report-agent.md);[面试题](../topics/ai/agent-design/interview.md) |
| [Agent 评测与运维](../topics/ai/agent-ops/index.md) | 课程:[Agent 评测和质量门禁](../topics/ai/agent-ops/01-eval-quality-gate.md);课程:[Agent 可观测性、成本和延迟](../topics/ai/agent-ops/02-observability-cost.md);场景:[客服 Agent 上线流程](../topics/ai/agent-ops/scenarios/agent-quality-gate.md);[面试题](../topics/ai/agent-ops/interview.md) |
| [模型侧基础(了解级)](../topics/ai/llm-serving/index.md) | 课程:[模型侧基础](../topics/ai/llm-serving/01-routing-vllm-lora.md);场景:[公司级大模型网关](../topics/ai/llm-serving/scenarios/multi-provider-gateway.md);[面试题](../topics/ai/llm-serving/interview.md) |
| [低代码 AI 平台(Dify / Coze)](../topics/ai/lowcode/index.md) | 课程:[Dify / Coze 低代码 AI 平台](../topics/ai/lowcode/01-dify-coze.md);场景:[从 Dify 原型到生产](../topics/ai/lowcode/scenarios/poc-to-production.md);[面试题](../topics/ai/lowcode/interview.md) |
| [提示词与上下文工程](../topics/ai/prompt-engineering/index.md) | 课程:[提示词注入和防护](../topics/ai/prompt-engineering/02-context-injection.md);场景:[读邮件的办公助手](../topics/ai/prompt-engineering/scenarios/injection-defense.md);[面试题](../topics/ai/prompt-engineering/interview.md) |
| [Docker 与 Compose](../topics/devops/docker/index.md) | 课程:[Compose、网络和数据卷](../topics/devops/docker/02-compose-network-volume.md);场景:[一条命令拉起开发环境](../topics/devops/docker/scenarios/compose-dev-env.md) |
| [Nginx](../topics/devops/nginx/index.md) | 场景:[AI 应用网关](../topics/devops/nginx/scenarios/ai-gateway-sse-canary.md) |
**跨技术栈场景**:[内容审核](../scenarios/content-moderation.md)、[客服知识库问答](../scenarios/customer-service-kb.md)、[多租户 SaaS](../scenarios/multi-tenant-saas.md)、[报表生成 Agent](../scenarios/report-agent.md)
<!-- week-topics:end -->