欢迎光临
我们一直在努力

【打造能干活的Agent】搭建可复现的 Python 任务助手环境

搭建可复现的 Python 任务助手环境:从空目录到可验收的运行骨架

课程归属:打造能干活的 Agent|第 4 节

刚开始开发 Agent 时,常见问题不是模型不够聪明,而是项目目录、Python 解释器、依赖和输出位置各不相同,导致“在我电脑上能跑”却难以复现。本文以 agent_lab/ 为统一项目,使用 WSL2 Ubuntu、Python 3.11 和标准库搭出一个可运行的资料分析任务助手骨架。完成后,你将得到固定目录、统一任务返回字段、示例输入,以及可检查的 report.md、summary.json 和 run.log;本节不接模型,也不提前引入 LangGraph。

一、这一节要完成什么

本节目标不是让 Agent 立刻理解任意资料,而是先固定后续章节共同依赖的运行契约:从哪里读资料、任务结果放在哪里、任务成功如何判定、环境问题如何定位。后面接入模型、工具循环和 SQLite 时,都在这个骨架上增量开发。

**验收结果:**在项目根目录运行一条命令,程序读取教学文本,生成三个任务产物,并返回固定字段的任务结果。整个示例不需要 API Key、GPU 或第三方 Python 包。

表 1:环境与本节边界

项目本节约定为什么这样定
操作环境 Windows 上的 WSL2 Ubuntu Linux 命令和路径在后续章节保持一致
Python 3.11 兼容环境 课程统一基线,不追逐最新版本
Python 依赖 标准库,无第三方包 首先排除依赖安装问题
输入目录 data/input/ 教学资料集中存放
输出目录 output// 每个任务独立归档,避免相互覆盖
本节不做 模型调用、LangGraph、数据库 按课程顺序逐步增加复杂度

二、先准备 WSL2 与 Python

如果电脑已经有 WSL2 Ubuntu,可以直接跳到版本检查。首次安装时,在 Windows PowerShell(管理员) 中运行:

wsl —install

按系统提示重启电脑,再从开始菜单打开 Ubuntu,创建 Linux 用户名和密码。该账号与 Windows 账号相互独立。若系统已安装 WSL,但没有 Ubuntu,可先查看可安装发行版,再指定安装:

wsl —list —online
wsl —install –d Ubuntu

这些是 Windows 侧命令;后续所有 apt、python 和 mkdir 命令都在 Ubuntu 终端 执行。微软官方安装说明指出,wsl –install 可启用所需组件并安装默认 Ubuntu,部分机器需要重启后才完成安装。Microsoft:安装 WSL

在 Ubuntu 中检查版本并安装基础工具:

sudo apt update
sudo apt install -y python3 python3-venv python3-pip git
python3 –version

最后一条应显示 Python 3.11.x。如果发行版自带的是其他版本,不要直接假定它符合课程基线;先检查 python3.11 –version 是否可用。若不可用,按所用 Ubuntu 版本的官方 Python 安装方式准备 Python 3.11,再继续。不要为了追求版本号而随意添加来源不明的软件源。

表 2:环境检查清单

检查项命令合格判据
WSL 发行版 Windows PowerShell:wsl -l -v Ubuntu 的 VERSION 为 2
Python 版本 Ubuntu:python3 –version 3.11.x
虚拟环境模块 python3 -m venv –help 显示帮助信息,无模块缺失错误
pip python3 -m pip –version 能显示 pip 版本与 Python 路径
工作位置 pwd 项目位于 Linux 家目录,例如 /home/<用户>/agent_lab

虚拟环境用于把本项目的 Python 包与系统 Python 分开。即使本节没有第三方依赖,也先建立 .venv,避免后续安装 SDK 时污染系统环境。Python 官方文档说明,venv 创建的环境相互隔离,常用目录名是 .venv。Python:虚拟环境

三、创建统一项目目录

在 Ubuntu 终端中执行以下命令。建议把项目放在 Linux 家目录中,例如 ~/agent_lab,而不是 /mnt/c/…;这样能减少跨文件系统开发时的路径和性能差异。

mkdir -p ~/agent_lab/data/input ~/agent_lab/output
cd ~/agent_lab
python3 -m venv .venv
source .venv/bin/activate
python –version
python -m pip –version

激活成功后,命令提示符通常会出现 (.venv)。每次重新打开终端,都需要重新激活:

cd ~/agent_lab
source .venv/bin/activate

项目结构如下。.venv/ 是本机生成的环境,不应提交到代码仓库;data/input/ 存放教学资料;output/ 下按任务 ID 创建结果目录。

agent_lab/
├── .venv/ # 本机虚拟环境,不提交版本库
├── .env.example # 后续模型配置模板,不放真实密钥
├── .gitignore # 忽略环境、密钥与运行产物
├── requirements.txt # Python 依赖清单
├── app.py # 本节可运行的任务助手骨架
├── data/
│ └── input/
│ └── brief.txt # 教学输入资料
└── output/
└── / # 每次任务的产物目录
├── report.md
├── summary.json
└── run.log

图 1:从开发环境到任务产物

#mermaid-svg-OP3UDM5tGnk7FL8B{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OP3UDM5tGnk7FL8B .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OP3UDM5tGnk7FL8B .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OP3UDM5tGnk7FL8B .error-icon{fill:#552222;}#mermaid-svg-OP3UDM5tGnk7FL8B .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OP3UDM5tGnk7FL8B .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OP3UDM5tGnk7FL8B .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OP3UDM5tGnk7FL8B .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OP3UDM5tGnk7FL8B .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OP3UDM5tGnk7FL8B .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OP3UDM5tGnk7FL8B .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OP3UDM5tGnk7FL8B .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OP3UDM5tGnk7FL8B .marker.cross{stroke:#333333;}#mermaid-svg-OP3UDM5tGnk7FL8B svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OP3UDM5tGnk7FL8B p{margin:0;}#mermaid-svg-OP3UDM5tGnk7FL8B .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-OP3UDM5tGnk7FL8B .cluster-label text{fill:#333;}#mermaid-svg-OP3UDM5tGnk7FL8B .cluster-label span{color:#333;}#mermaid-svg-OP3UDM5tGnk7FL8B .cluster-label span p{background-color:transparent;}#mermaid-svg-OP3UDM5tGnk7FL8B .label text,#mermaid-svg-OP3UDM5tGnk7FL8B span{fill:#333;color:#333;}#mermaid-svg-OP3UDM5tGnk7FL8B .node rect,#mermaid-svg-OP3UDM5tGnk7FL8B .node circle,#mermaid-svg-OP3UDM5tGnk7FL8B .node ellipse,#mermaid-svg-OP3UDM5tGnk7FL8B .node polygon,#mermaid-svg-OP3UDM5tGnk7FL8B .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OP3UDM5tGnk7FL8B .rough-node .label text,#mermaid-svg-OP3UDM5tGnk7FL8B .node .label text,#mermaid-svg-OP3UDM5tGnk7FL8B .image-shape .label,#mermaid-svg-OP3UDM5tGnk7FL8B .icon-shape .label{text-anchor:middle;}#mermaid-svg-OP3UDM5tGnk7FL8B .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OP3UDM5tGnk7FL8B .rough-node .label,#mermaid-svg-OP3UDM5tGnk7FL8B .node .label,#mermaid-svg-OP3UDM5tGnk7FL8B .image-shape .label,#mermaid-svg-OP3UDM5tGnk7FL8B .icon-shape .label{text-align:center;}#mermaid-svg-OP3UDM5tGnk7FL8B .node.clickable{cursor:pointer;}#mermaid-svg-OP3UDM5tGnk7FL8B .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OP3UDM5tGnk7FL8B .arrowheadPath{fill:#333333;}#mermaid-svg-OP3UDM5tGnk7FL8B .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OP3UDM5tGnk7FL8B .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OP3UDM5tGnk7FL8B .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OP3UDM5tGnk7FL8B .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OP3UDM5tGnk7FL8B .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OP3UDM5tGnk7FL8B .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OP3UDM5tGnk7FL8B .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OP3UDM5tGnk7FL8B .cluster text{fill:#333;}#mermaid-svg-OP3UDM5tGnk7FL8B .cluster span{color:#333;}#mermaid-svg-OP3UDM5tGnk7FL8B div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OP3UDM5tGnk7FL8B .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OP3UDM5tGnk7FL8B rect.text{fill:none;stroke-width:0;}#mermaid-svg-OP3UDM5tGnk7FL8B .icon-shape,#mermaid-svg-OP3UDM5tGnk7FL8B .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OP3UDM5tGnk7FL8B .icon-shape p,#mermaid-svg-OP3UDM5tGnk7FL8B .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OP3UDM5tGnk7FL8B .icon-shape .label rect,#mermaid-svg-OP3UDM5tGnk7FL8B .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OP3UDM5tGnk7FL8B .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OP3UDM5tGnk7FL8B .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OP3UDM5tGnk7FL8B :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

WSL2 Ubuntu

Python 3.11

项目虚拟环境 .venv

agent_lab 任务助手

data/input 教学资料

output/task_id 任务目录

report.md

summary.json

run.log

这张图强调环境、输入和产物是三个不同层次:虚拟环境解决依赖隔离,输入目录保存源资料,任务目录保存本次运行的证据。不要把生成报告放回输入目录,否则重复运行时容易把产物误当成新输入。

四、补齐配置与示例资料

先创建 requirements.txt。本节只使用 Python 标准库,因此文件可以为空;为了便于读者理解,也可以保留注释:

# 本节仅使用 Python 标准库,无第三方依赖。
# 后续章节会在核验 SDK 版本后补充精确依赖约束。

创建 .env.example:

# 本节不发起模型请求,以下仅为后续章节的配置占位模板。
# 复制为 .env 后再填写你有权使用的模型服务信息。
MODEL_BASE_URL=https://example.invalid/v1
MODEL_NAME=replace-with-your-model-name
MODEL_API_KEY=replace-with-your-api-key

example.invalid 是用于示例的保留域名,不是可用接口地址。本节不会读取或发送这些变量;后续接入模型时,再将其替换成经官方文档核验的服务端点和模型名。不要把真实密钥提交到 Git,也不要把密钥写入源代码。

创建 .gitignore:

.venv/
.env
__pycache__/
*.py[cod]
output/*/
!output/.gitkeep

如果希望版本库保留空的 output/ 目录,可创建 output/.gitkeep。然后创建教学输入文件 data/input/brief.txt:

资料名称:每周工作摘要(教学模拟)

本周完成:
– 整理五条模拟业务记录。
– 检查输入文件是否存在。
– 将任务摘要写入独立输出目录。

注意:本文内容仅用于演示项目环境和文件读写,不代表真实业务数据。

表 3:本节文件职责

文件/目录用途是否包含秘密
requirements.txt 记录 Python 依赖 否
.env.example 展示未来需要的变量名 否,只有占位值
.env 本机实际配置(后续章节使用) 可能包含密钥,不提交
data/input/brief.txt 可重复使用的教学输入 否
output// 报告、摘要和运行日志 可能含业务信息,按数据敏感度管理

五、实现最小任务助手

创建 app.py。为了让后续章节可以直接扩展,本节先固定公共接口和返回字段:run_task(task_input, task_id) 返回任务结果字典;read_text_file() 返回逻辑工具结果。工具的 status=ok 表示工具调用成功,不等同于整个任务 status=completed。本节还没有模型规划器,任务步骤是确定性的 Python 流程。

from __future__ import annotations

import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

ROOT = Path(__file__).resolve().parent
INPUT_DIR = ROOT / "data" / "input"
OUTPUT_DIR = ROOT / "output"
DEFAULT_INPUT = "brief.txt"

def utc_now() –> str:
"""返回 UTC ISO 8601 时间,供日志记录使用。"""
return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")

def safe_task_id(value: str | None) –> str:
"""生成或校验任务 ID,避免路径穿越。"""
candidate = value or datetime.now(timezone.utc).strftime("task-%Y%m%dT%H%M%SZ")
if not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", candidate):
raise ValueError("task_id 仅允许 1-64 位字母、数字、下划线和连字符")
return candidate

def read_text_file(relative_path: str) –> dict[str, Any]:
"""读取 data/input 下的文本,返回统一逻辑工具结果。"""
try:
base = INPUT_DIR.resolve()
target = (base / relative_path).resolve()
if not target.is_relative_to(base):
raise ValueError("文件路径必须位于 data/input 目录内")
if not target.is_file():
return {
"status": "error", "data": None,
"source": [relative_path],
"error": {"code": "FILE_NOT_FOUND", "message": "输入文件不存在"},
}
text = target.read_text(encoding="utf-8")
return {
"status": "ok",
"data": {"relative_path": relative_path, "text": text},
"source": [relative_path],
"error": None,
}
except (OSError, UnicodeError, ValueError) as exc:
return {
"status": "error", "data": None,
"source": [relative_path],
"error": {"code": "READ_ERROR", "message": str(exc)},
}

def run_task(task_input: str, task_id: str | None = None) –> dict[str, Any]:
"""执行一个确定性演示任务,并写入三个约定产物。"""
task_id = safe_task_id(task_id)
task_dir = OUTPUT_DIR / task_id
task_dir.mkdir(parents=True, exist_ok=True)
# 同一 task_id 重跑时先清除旧产物,避免失败后残留旧报告造成误判。
for name in ("report.md", "summary.json"):
(task_dir / name).unlink(missing_ok=True)
log_lines = [f"[{utc_now()}] task_id={task_id} status=running"]
warnings: list[str] = []
report_path: str | None = None
summary: dict[str, Any] = {}

try:
tool_result = read_text_file(DEFAULT_INPUT)
if tool_result["status"] != "ok":
raise RuntimeError(tool_result["error"]["message"])

source_text = tool_result["data"]["text"]
summary = {
"task_id": task_id,
"input_file": DEFAULT_INPUT,
"input_chars": len(source_text),
"task_input": task_input,
"source_count": len(tool_result["source"]),
}
report = (
f"# 任务报告:{task_id}\\n\\n"
f"- 任务说明:{task_input}\\n"
f"- 输入文件:`{DEFAULT_INPUT}`\\n"
f"- 输入字符数:{len(source_text)}\\n\\n"
"## 输入内容\\n\\n"
f"{source_text.strip()}\\n\\n"
"## 说明\\n\\n"
"本报告由确定性的 Python 骨架生成;尚未调用大模型,也不代表模型分析结论。\\n"
)
(task_dir / "report.md").write_text(report, encoding="utf-8")
(task_dir / "summary.json").write_text(
json.dumps(summary, ensure_ascii=False, indent=2) + "\\n",
encoding="utf-8",
)
report_path = f"output/{task_id}/report.md"
log_lines.append(f"[{utc_now()}] input={DEFAULT_INPUT} status=ok")
log_lines.append(f"[{utc_now()}] report={report_path} status=written")
log_lines.append(f"[{utc_now()}] task_id={task_id} status=completed")
status = "completed"
except Exception as exc:
warnings.append(str(exc))
summary = {"task_id": task_id}
log_lines.append(f"[{utc_now()}] status=failed error={type(exc).__name__}: {exc}")
status = "failed"
finally:
(task_dir / "run.log").write_text("\\n".join(log_lines) + "\\n", encoding="utf-8")

return {
"task_id": task_id,
"status": status,
"report_path": report_path,
"summary": summary,
"warnings": warnings,
}

def main() –> int:
task_input = "检查教学资料并生成环境验收报告"
if len(sys.argv) > 1:
task_input = " ".join(sys.argv[1:]).strip()
result = run_task(task_input, task_id="env-check-001")
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0 if result["status"] == "completed" else 1

if __name__ == "__main__":
raise SystemExit(main())

这里有三个有意为之的边界:第一,读取路径被限制在 data/input/,不允许通过 ../ 读取项目外文件;第二,任务 ID 经过字符白名单校验,不能被用作路径穿越;第三,只有报告和摘要成功写入后才返回 completed。失败时仍尽量写 run.log,并通过 warnings 返回原因。

图 2:任务骨架的执行与失败分支

#mermaid-svg-k8LiM6RV4HSZ1bAQ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-k8LiM6RV4HSZ1bAQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .error-icon{fill:#552222;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .marker.cross{stroke:#333333;}#mermaid-svg-k8LiM6RV4HSZ1bAQ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-k8LiM6RV4HSZ1bAQ p{margin:0;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .cluster-label text{fill:#333;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .cluster-label span{color:#333;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .cluster-label span p{background-color:transparent;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .label text,#mermaid-svg-k8LiM6RV4HSZ1bAQ span{fill:#333;color:#333;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .node rect,#mermaid-svg-k8LiM6RV4HSZ1bAQ .node circle,#mermaid-svg-k8LiM6RV4HSZ1bAQ .node ellipse,#mermaid-svg-k8LiM6RV4HSZ1bAQ .node polygon,#mermaid-svg-k8LiM6RV4HSZ1bAQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .rough-node .label text,#mermaid-svg-k8LiM6RV4HSZ1bAQ .node .label text,#mermaid-svg-k8LiM6RV4HSZ1bAQ .image-shape .label,#mermaid-svg-k8LiM6RV4HSZ1bAQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .rough-node .label,#mermaid-svg-k8LiM6RV4HSZ1bAQ .node .label,#mermaid-svg-k8LiM6RV4HSZ1bAQ .image-shape .label,#mermaid-svg-k8LiM6RV4HSZ1bAQ .icon-shape .label{text-align:center;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .node.clickable{cursor:pointer;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .arrowheadPath{fill:#333333;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-k8LiM6RV4HSZ1bAQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-k8LiM6RV4HSZ1bAQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-k8LiM6RV4HSZ1bAQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .cluster text{fill:#333;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .cluster span{color:#333;}#mermaid-svg-k8LiM6RV4HSZ1bAQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-k8LiM6RV4HSZ1bAQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .icon-shape,#mermaid-svg-k8LiM6RV4HSZ1bAQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .icon-shape p,#mermaid-svg-k8LiM6RV4HSZ1bAQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .icon-shape .label rect,#mermaid-svg-k8LiM6RV4HSZ1bAQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-k8LiM6RV4HSZ1bAQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-k8LiM6RV4HSZ1bAQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-k8LiM6RV4HSZ1bAQ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

是

是

否

否

调用 run_task

校验 task_id

读取 data/input/brief.txt

读取成功?

生成报告与摘要

产物写入成功?

写入日志并返回 completed

记录 warning

写入失败日志并返回 failed

completed 不是“代码走到了函数末尾”,而是代表核心产物已写入。实际系统还应校验文件是否存在、JSON 是否可解析、报告是否非空;下一节会在环境验收命令中执行这些检查。

六、运行并验证产物

确认当前终端位于 ~/agent_lab 且虚拟环境已激活。执行语法检查和任务:

python -m py_compile app.py
python app.py

程序会打印任务结果 JSON。以下是预期输出形态示例,时间和 input_chars 会随实际文件内容变化;它不是本篇已执行的运行截图或实测日志:

{
"task_id": "env-check-001",
"status": "completed",
"report_path": "output/env-check-001/report.md",
"summary": {
"task_id": "env-check-001",
"input_file": "brief.txt",
"input_chars": 180,
"task_input": "检查教学资料并生成环境验收报告",
"source_count": 1
},
"warnings": []
}

不要只看终端里的 completed。继续验证三个文件确实存在、JSON 可解析、报告内容非空:

test -s output/env-check-001/report.md
test -s output/env-check-001/summary.json
test -s output/env-check-001/run.log
python – <<'PY'
import json
from pathlib import Path
p = Path("output/env-check-001")
summary = json.loads((p / "summary.json").read_text(encoding="utf-8"))
assert summary["task_id"] == "env-check-001"
assert summary["input_file"] == "brief.txt"
assert (p / "report.md").read_text(encoding="utf-8").strip()
assert (p / "run.log").read_text(encoding="utf-8").strip()
print("产物检查完成:report.md、summary.json、run.log 均存在且可读取")
PY

表 4:验收标准

验收项检查方法成功判据
Python 语法 python -m py_compile app.py 命令退出码为 0
任务状态 查看终端 JSON status 为 completed
报告文件 test -s …/report.md 文件存在且非空
摘要格式 json.loads() JSON 可解析,task_id 一致
日志文件 test -s …/run.log 文件存在且含任务状态记录

图 3:从命令到证据的验收链

"output/task_id"

"data/input"

"app.py"

"开发者"

"output/task_id"

"data/input"

"app.py"

"开发者"

#mermaid-svg-KpZ7Mv3682sgU8OB{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KpZ7Mv3682sgU8OB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KpZ7Mv3682sgU8OB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KpZ7Mv3682sgU8OB .error-icon{fill:#552222;}#mermaid-svg-KpZ7Mv3682sgU8OB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KpZ7Mv3682sgU8OB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KpZ7Mv3682sgU8OB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KpZ7Mv3682sgU8OB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KpZ7Mv3682sgU8OB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KpZ7Mv3682sgU8OB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KpZ7Mv3682sgU8OB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KpZ7Mv3682sgU8OB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KpZ7Mv3682sgU8OB .marker.cross{stroke:#333333;}#mermaid-svg-KpZ7Mv3682sgU8OB svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KpZ7Mv3682sgU8OB p{margin:0;}#mermaid-svg-KpZ7Mv3682sgU8OB .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KpZ7Mv3682sgU8OB text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-KpZ7Mv3682sgU8OB .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-KpZ7Mv3682sgU8OB .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-KpZ7Mv3682sgU8OB .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-KpZ7Mv3682sgU8OB .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-KpZ7Mv3682sgU8OB #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-KpZ7Mv3682sgU8OB .sequenceNumber{fill:white;}#mermaid-svg-KpZ7Mv3682sgU8OB #sequencenumber{fill:#333;}#mermaid-svg-KpZ7Mv3682sgU8OB #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-KpZ7Mv3682sgU8OB .messageText{fill:#333;stroke:none;}#mermaid-svg-KpZ7Mv3682sgU8OB .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KpZ7Mv3682sgU8OB .labelText,#mermaid-svg-KpZ7Mv3682sgU8OB .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-KpZ7Mv3682sgU8OB .loopText,#mermaid-svg-KpZ7Mv3682sgU8OB .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-KpZ7Mv3682sgU8OB .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-KpZ7Mv3682sgU8OB .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-KpZ7Mv3682sgU8OB .noteText,#mermaid-svg-KpZ7Mv3682sgU8OB .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-KpZ7Mv3682sgU8OB .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KpZ7Mv3682sgU8OB .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KpZ7Mv3682sgU8OB .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KpZ7Mv3682sgU8OB .actorPopupMenu{position:absolute;}#mermaid-svg-KpZ7Mv3682sgU8OB .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-KpZ7Mv3682sgU8OB .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KpZ7Mv3682sgU8OB .actor-man circle,#mermaid-svg-KpZ7Mv3682sgU8OB line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-KpZ7Mv3682sgU8OB :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

运行 python app.py

读取 brief.txt

返回教学文本

写入 report.md

写入 summary.json

写入 run.log

返回任务结果 JSON

检查文件与 JSON

提供可核对的运行证据

终端返回值是接口证据,输出文件是交付证据,两者都要检查。后续加入模型后,这条验收链仍然适用:模型说“完成”并不能替代文件、结构化结果和日志的实际验证。

七、做一次失败用例,确认错误可定位

现在把默认输入文件临时改名,验证程序不会把缺少输入误报成成功。请先确认该文件是本教程创建的教学文件,再执行:

mv data/input/brief.txt data/input/brief.txt.bak
python app.py; echo "exit_code=$?"
cat output/env-check-001/run.log
mv data/input/brief.txt.bak data/input/brief.txt

预期任务状态是 failed,warnings 中包含输入文件不存在的提示,日志中出现失败原因。同一 task_id 重跑前,程序会清除旧的报告与摘要,避免失败时旧文件被误认为本次成功产物。测试后恢复 brief.txt 并重新运行 python app.py,即可恢复正常结果。若要保留失败现场,可先把输出目录改名归档,再执行测试。

表 5:常见故障与处理

现象常见原因处理方式
python: command not found Python 未安装或命令名不同 检查 python3 –version,用 python3 创建环境
No module named venv 未安装 venv 支持包 Ubuntu 中安装 python3-venv
激活后仍使用系统 Python 没有激活项目虚拟环境 进入项目根目录重新执行 source .venv/bin/activate
FILE_NOT_FOUND / 输入文件不存在 文件名拼错或文件不在输入目录 检查 ls -l data/input/,恢复 brief.txt
任务 ID 校验失败 ID 含空格、斜杠或其他字符 使用字母、数字、下划线或连字符
JSON 解析失败 文件被手动编辑或写入不完整 重新运行任务生成摘要,再执行解析检查

八、这一环境如何承接后续章节

本节把目录和接口先固定下来,不意味着所有后续能力都已经实现。接入大模型后,会在 .env 中配置模型服务信息;把函数封装成工具后,沿用 status/data/source/error 的逻辑结果契约;引入 LangGraph 后,再将当前确定性流程替换为可观察、可控的多步执行流程;进入 SQLite 章节时,才创建 data/agent.db 并按课程公共表结构持久化任务状态。

表 6:后续扩展与当前边界

后续能力本节准备了什么本节尚未实现什么
模型对话 .env.example 变量名占位 未请求模型、未核验具体模型权限或费用
工具调用 read_text_file() 逻辑结果结构 尚未让模型决定何时调用工具
多步编排 统一 run_task() 接口 尚未引入 LangGraph 或自动规划循环
任务持久化 task_id 与输出目录约定 尚未创建 SQLite 数据库
可视化界面 任务结果字段已固定 尚未引入 Streamlit

完成本节后,你应能从空目录重建项目、激活独立 Python 环境、运行任务并用文件验证结果。下一步再接入国产模型服务,才有清晰的配置位置、调用入口和故障边界。

参考资料

  • Microsoft Learn:安装 WSL
  • Microsoft Learn:设置 WSL 开发环境
  • Microsoft Learn:使用 VS Code 与 WSL 开发
  • Python 3.11 官方文档:虚拟环境与包
  • Python 官方发布说明:Python 3.11.17 等维护版本发布公告
  • 赞(0)
    未经允许不得转载:171主机测评 » 【打造能干活的Agent】搭建可复现的 Python 任务助手环境
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址