在 LLM 应用里,模型当然重要,但真正决定开发流程能否稳定复用的,往往是模型外围的 harness(编排与运行框架)。它负责把提示词、工具调用、上下文、权限、重试、日志和人工确认组织成一个可观测的闭环。模型可以替换,流程不能靠临场发挥。
本期话题同时谈到 agentic 开发工作流、Scrapy 网页抓取和 Python 应用自托管。把它们放在一起看,会得到一个很实用的结论:LLM 应该消费经过约束、清洗和追踪的数据,而不是直接面对不可控的网页与生产环境。
模型是推理引擎,Harness 才是操作系统
把一个开发型 agent 想象成一名能快速写代码的工程师。模型决定它能否理解需求、生成代码和分析错误;harness 则规定它:
- 从哪里读取任务和代码上下文;
- 哪些命令、文件和网络请求可以执行;
- 工具失败后如何重试、降级或停止;
- 什么时候必须请求人工确认;
- 如何记录输入、工具调用、输出和最终变更。
没有 harness 的聊天式工作流通常很脆弱:一次上下文遗漏、一次命令误执行,或一次工具返回格式变化,都可能让结果失去可重复性。
一个可维护的 harness 不必从复杂框架开始。核心是把不稳定部分隔离出去:模型供应商、提示词模板、工具执行器、状态存储和审核策略应当有明确边界。这样更换模型时,测试和权限控制不需要跟着重写。
Agentic 开发流程应当分层,而不是放开权限
对代码库进行自动化修改时,最危险的设计是让 agent 同时拥有无限上下文、任意 shell 权限和自动提交能力。更稳妥的流程可以分成四步:
- 收集:读取 issue、相关源码、测试和运行日志。
- 规划:产出结构化修改计划,明确会触碰的文件与验证方式。
- 执行:通过受限工具写入工作区,并运行允许列表中的命令。
- 验收:执行测试、汇总 diff,交由人或规则引擎决定是否合并。
这里的关键不是让 agent 多做几步,而是让每一步都有可检查的输入和输出。例如,规划阶段输出 JSON,比输出一段自由文本更容易交给后续程序校验。
可以这样实践一个最小任务契约:
{
"task": "为用户接口补充分页测试",
"allowed_paths": ["src/api/users.py", "tests/test_users.py"],
"allowed_commands": ["pytest -q tests/test_users.py"],
"requires_review": true
}
这个契约不能阻止所有错误,但能把 agent 的活动范围压缩到可审计边界内。尤其在自托管环境中,路径限制、命令白名单和密钥隔离应当被视为基础设施,而不是后补功能。
用 Scrapy 把网页变成可消费的数据源
网页抓取很适合作为 LLM 工作流的上游,但原始 HTML 不应该直接塞进上下文窗口。导航栏、广告、重复模块和过期内容会浪费 token,也会干扰检索与回答。
Scrapy 提供了较清晰的抓取结构:请求调度、CSS/XPath 选择器、限速、导出和中间件可以各自配置。下面是一个最小项目,抓取允许访问的文档页面标题与正文,并导出为 JSON Lines。
先创建虚拟环境并安装依赖:
python -m venv .venv
source .venv/bin/activate
pip install scrapy
scrapy startproject docsbot
cd docsbot
将下面内容保存为 docsbot/spiders/docs.py。运行前将 example.com 和选择器替换为你有权抓取的网站与实际页面结构:
import scrapy
class DocsSpider(scrapy.Spider):
name = "docs"
allowed_domains = ["example.com"]
start_urls = ["https://example.com/docs/"]
custom_settings = {
"DOWNLOAD_DELAY": 1.0,
"ROBOTSTXT_OBEY": True,
"USER_AGENT": "docsbot/0.1 (+contact@example.com)",
}
def parse(self, response):
for href in response.css("main a::attr(href)").getall():
yield response.follow(href, callback=self.parse_page)
def parse_page(self, response):
title = response.css("h1::text").get(default="").strip()
paragraphs = response.css("main p::text").getall()
content = "\n".join(text.strip() for text in paragraphs if text.strip())
if title and content:
yield {
"url": response.url,
"title": title,
"content": content,
}
执行抓取:
scrapy crawl docs -O documents.jsonl
生成的每一行都是一个 JSON 文档,适合后续做去重、分块、嵌入和检索。实际接入 LLM 前,建议额外保留 url、抓取时间、内容哈希和文档版本;当回答需要引用或数据发生变化时,这些字段能帮助定位来源并增量更新索引。
自托管时,把运行边界当作产品能力
自托管 Python 应用的价值不只是省去托管平台费用。对于包含内部代码、业务文档或抓取数据的 agent 系统,它意味着可以控制数据驻留位置、网络出口、日志保留策略和模型调用路径。
但自托管也会把责任带回来:升级、备份、TLS、监控、任务队列和密钥轮换都需要明确负责人。一个常见的务实做法是先拆分状态:Web 服务保持无状态,抓取与索引任务交给 worker,文档和任务状态落到受备份保护的数据库或对象存储。
下面的 Compose 配置展示了一个可改造的本地结构:应用服务负责 API,worker 负责异步任务。示例使用环境变量引用密钥,避免把密钥写进镜像或仓库。
services:
api:
image: my-agent-app:latest
command: uvicorn app.main:app --host 0.0.0.0 --port 8000
ports:
- "8000:8000"
environment:
DATABASE_URL: ${DATABASE_URL}
LLM_API_KEY: ${LLM_API_KEY}
restart: unless-stopped
worker:
image: my-agent-app:latest
command: python -m app.worker
environment:
DATABASE_URL: ${DATABASE_URL}
LLM_API_KEY: ${LLM_API_KEY}
restart: unless-stopped
生产部署还应补上反向代理、健康检查、最小权限运行用户、出站网络策略和备份演练。特别是带有 shell 或浏览器工具的 agent,容器隔离并不等于权限安全,仍需限制挂载目录、系统调用和可访问的凭据。
从一个闭环开始验证
不必一开始就构建全自动开发团队。更合适的起点是一个闭环:Scrapy 抓取一类已授权文档,清洗后写入索引;agent 只能检索这些文档,并通过受限工具生成一个补丁或报告;最后由测试和人工审核决定结果是否可用。
采用时可以检查四件事:
- 模型替换后,工具接口、任务状态和审计日志是否仍然有效;
- 每次抓取是否遵守站点规则、频率限制和数据使用边界;
- 每个自动化动作是否有最小权限、可追踪记录和停止条件;
- 自托管环境是否能恢复数据、轮换密钥并定位失败任务。
模型能力会持续变化,但可靠的 harness、干净的数据管道和清晰的运行边界,会让这些变化真正转化为可交付的工程效率。