
从2023年开始AI Agent这个概念就不断刷屏但大部分演示都停留在“问一句答一句”的阶段真正能把工具调用、信息检索、定时任务串成一条龙的产品少之又少。我花了三周时间做了个叫 Agent-Reach 的小项目核心思路极其直白让智能体不只是“聊得动”更要“摸得到”——能自己上网查资料、读文档、订闹钟式的定时任务还能把结果写成文件丢到指定目录。这篇文章把整个项目的架构选型、核心细节、实操过程和踩坑记录全部分享出来东西不复杂但每个环节都有值得说道的地方。我原本的诉求很简单每天早晨能有一个助理帮我把当天要看的行业动态、竞品价格变化、几个关键指数的波动汇总成一份简报。市面上现成的RSS阅读器和通知机器人其实能做到一部分但灵活性太差我想要的是“能根据昨天的情况决定今天看什么”的智能调度。于是就有了 Agent-Reach 的雏形——一个具备实时搜索能力、本地记忆检索能力、浏览器自动化操作能力和定时触发能力的轻量级智能体跑在一台常开的迷你主机上通过Web界面做交互和结果查看。这个项目适合三类人参考一是想在本地搭一个私有AI助手的开发者二是需要自动化处理信息收集类工作的效率控三是做RAG或Agent相关技术验证的工程师。不太适合指望开箱即用的纯用户因为它需要一定的Python和API配置基础但整体的架构思路和代码量都不算吓人。1. 整体设计与能力边界规划1.1 为什么叫“Agent-Reach”它到底解决了什么问题给项目起名的时候“Reach”这个词其实有两层含义。字面意思是“到达、触达”也就是让智能体拥有获取外部信息、操作外部系统的能力另一层意思是“能力范围”强调的是这个智能体到底能把触角伸多远。市面上的聊天机器人大多只有“语言能力”没有“行动能力”你问它今天天气它会说“请开启联网搜索”你让它帮你订个会议室它就完全没辙。Agent-Reach 的目标就是补齐这个断层。我最终确认的四大核心能力是外部实时检索接入搜索API和新闻源、本地私有资料问答基于向量库的RAG、浏览器自动化任务用Playwright控制浏览器执行操作、定时触发与结果推送类似一个简易版的自动化工作流。这四件事单独拎出来都有成熟的工具但把它们拼在一个Agent调度框架里并且让模型能够在对话中自主决定“该调哪个工具、按什么顺序调、参数该填什么”这才是真正的难点。1.2 工具选型背后的取舍为什么选择这套组合方案技术栈的选型我纠结了很久反复推了两版方案。第一版想用一套非常重的LangChain生态配合LangGraph做状态机编排。LangChain的好处是生态全各种工具都有现成的集成坏处是抽象层次太多出了问题排查链路很长而且版本更新频繁三周后很可能我写的代码就变成了“过时写法”。对于一个小型个人项目来说这种重架构就像用集装箱卡车去拉一车快递本身成本大于收益。最终我采用了相对务实的组合模型层用openai兼容接口本地部署可以用 Ollama 跑 Qwen 或 Llama也可以用任意云端 APIAgent调度层自己写一个只有几百行代码的Tool-Calling循环工具层四个模块各自独立封装通信全部走统一的run_tool(name, args)接口。这个架构的核心优势是透明、可改、可控。自写调度循环保证了每一步调用都能看到完整上下文出了错可以手工干预工具层独立封装则让后续增删工具像插拔U盘一样简单。模块方案选型选型理由模型推理OpenAI兼容接口 / Ollama本地模型灵活切换成本可控Agent调度自写 Tool-Calling 循环轻量透明便于调试外部检索SearXNG 自建搜索API隐私可控免费无限制文档记忆Chroma 向量库 BGE嵌入本地运行无需GPU也能用浏览器操作Playwright稳定性强支持Stealth定时调度APScheduler和Python生态无缝整合Web界面FastAPI Vue单页开发效率高所见即所得这个组合是典型的“够用就好”哲学每块都不是最强的最厉害的工具但合在一起能稳定运行且所有部件都在本地可控范围内没有外部依赖的黑盒。尤其重要的是这套方案的运行门槛很低——一台4核8G内存的小主机完全跑得动算上所有服务内存占用大概在2.5GB左右。2. 核心细节解析与实操要点2.1 工具调用Function Calling的实现方式不止是套API大模型本身的Function Calling能力才是整个Agent-Reach的地基。不管是OpenAI、Claude还是本地运行的Qwen都提供了结构化工具调用能力模型在回答用户问题时可以输出一个“我想调用某个工具参数是这样”的特殊指令而不是直接输出纯文本。系统收到这个指令后去真实执行工具再把结果回传给模型让模型基于结果继续生成回答。在设计工具Schema时我踩了一个明显的坑一开始把参数定义写得非常宽松类似“query: 用户想搜索的内容”结果模型经常漏填关键参数或者填出一些根本没有意义的内容。后来我改成把每个参数都加上详细描述和校验规则并且在系统提示词里写了明确的使用约束。比如搜索工具参数不仅是keyword还加了time_range、region、language每个参数都说明了合法取值范围。加了这些约束后工具调用的准确率从肉眼可见的不到六成提升到了接近九成。自己写调度循环的核心代码并不复杂思路是把历史消息和可用工具列表一起发给模型模型返回一个结构化响应如果有工具调用就解析出工具名称和参数执行后把结果以tool角色的消息追加到历史消息里再次调用模型重复这个过程直到模型不再请求调用工具。这个循环要注意设置最大轮数我设的是6轮否则模型可能在一个问题上反复调工具直到把token耗尽。def run_agent_loop(user_message: str, max_rounds: int 6): messages [{role: system, content: SYSTEM_PROMPT}] messages.append({role: user, content: user_message}) for _ in range(max_rounds): resp llm.chat(messagesmessages, toolsTOOL_SCHEMAS) msg resp.choices[0].message if msg.tool_calls: messages.append(msg) for tc in msg.tool_calls: result execute_tool(tc.function.name, tc.function.arguments) messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps(result, ensure_asciiFalse) }) else: return msg.content return 已达到最大工具调用轮数请缩小问题范围后重试。这里有个容易被忽略的细节工具执行结果回传给模型时不建议把整个原始返回直接丢进去。搜索API返回的HTML或者JSON可能有几十KB塞进上下文既浪费token又干扰模型判断。我写的execute_tool内部做了一层提炼比如搜索只保留标题、链接、摘要前三项网页抓取只提取正文文本并截断到2000字符。上下文干净了模型的选择明显更精准。2.2 记忆与检索轻量RAG不要陷入“洗数据”泥潭Agent-Reach 除了能上网搜信息还要能用上本地积累的资料——比如我自己整理的产品文档、每周保存的行业报告、会议记录等等。这块做的是预设知识库问答技术上就是经典的RAG把文档切块、做向量化、存入向量库用户提问时先检索出最相关的几块内容和问题一起送给模型做作答。一开始我犯了个贪大求全的毛病想把所有历史资料一股脑塞进去做全量向量化结果发现有些扫描版PDF清洗起来没完没了切出来的文本质量惨不忍睹。调试了两天后我明白了RAG项目的核心不在“向量化”而在“文档预处理”。凡是OCR质量差、排版混乱的文件检索出来的内容反而会误导模型。因此我把知识库的定位从“所有资料”调整成了“经过筛选的优质资料”每个文档进库前都要做格式化清理和人工抽检宁缺毋滥。嵌入模型我最终选了BGE-small-zh一个参数量不大但中文效果不错的模型跑在CPU上单条向量化大约需要150毫秒左右完全可以接受。向量库用Chroma主要原因是我图它无需独立服务、库内带持久化、配置起来十分钟内能搞定。检索时在Chroma里做相似度搜索取Top-K4再加上一个相似度阈值过滤低于0.4的结果直接丢弃防止模型拿着毫不相关的内容硬编。在对话时Agent的调度逻辑会先判断这个问题是否需要检索本地知识要不要检索、检索哪个知识库、怎么构造检索query这三件事本身也交给模型做决策。这种“先决策再检索再回答”的流程比“无脑把所有内容塞给模型”要高效得多回答质量和token开销都改善明显。2.3 浏览器自动化与Stealth模式容易被低估的难点Agent-Reach 里最重的一个工具是浏览器自动化。我拿它做了一件事定时去几个指定网站抓取竞品价格变化顺便把需要登录才能看到的信息比如后台报表自动截图存档。这部分完全基于Playwright实现选择它而不是Selenium很大程度上因为Playwright的API更现代、等待机制更可靠、对Chromium的控制更彻底。真正难搞的是反爬和登录态保持。很多网站都有基础的反自动化检测WebDriver属性、自动化相关JavaScript变量、异常的时间指纹都会被识别。我引入了playwright-stealth补丁同时手动设置了一个更接近真实用户的环境固定用户代理、跳过webdriver标记、设置真实屏幕分辨率、随机化操作间隔。这套组合不能说百分百可靠但实测下来主流网站的拦截率大幅降低。登录态保持我用的是持久化上下文。创建浏览器上下文时指定user_data_dir参数第一次手工登录后Cookie和LocalStorage会被Playwright持久化到本地目录后续每次启动都复用这个上下文大部分站点就不再需要重新登录。对于有会话过期机制的网站我加了一个尝试判断页面元素是否出现的逻辑一旦发现跳转到了登录页就发送通知让我手动介入。这个工具模块的代码结构上我封装了一个BrowserTool类内部管理浏览器的生命周期。重点是把所有常用操作打开页面、点击、输入、等待特定元素、截图、提取表格都写成了独立方法这样上层Agent在调度时不需要关心浏览器内部状态只需告诉它“去这个URL把这个CSS选择器对应的文本提取出来”。实操时建议把每个浏览器动作都配上超时控制默认15秒超时立即抛异常返回给模型而不是傻等——让Agent知道“当前操作失败了换个思路”。3. 实操过程与核心环节实现3.1 前置环境准备一台常开主机和一小时基础配置我用了手头一台闲置的迷你主机配置是AMD R5 CPU、16GB内存、512GB SSD系统装的Ubuntu 22.04 Server。如果你不想常开一台机器也可以跑在云服务器上但要注意隐私数据不要放云端。具体环境准备步骤如下安装Python 3.11和虚拟环境工具创建独立venv避免污染系统Python版本。安装依赖包openai、fastapi、uvicorn、chromadb、playwright、apscheduler、requests、beautifulsoup4、pydantic。安装Playwright的Chromium内核命令是playwright install chromium。如果跑在有图形界面的机器上会把视频输出关掉省资源。配置SearXNG。如果你不想自己部署SearXNG可以用一些公共搜索API但自建的好处是免费且访问稳定。Docker一条命令可以拉起docker run -d -p 8888:8080 searxng/searxng然后修改配置文件允许JSON格式返回。下载BGE嵌入模型。用sentence-transformers拉取BAAI/bge-small-zh-v1.5首次运行会自动下载之后会缓存到本地。这些准备步骤看起来多其实核心就是“装软件、起服务、测连通”。我第一次搭建整个环境花了一个多小时一半时间都耗在调试SearXNG的JSON返回上——它默认配置是关闭JSON格式的需要在settings.yml里显式加上json格式支持并且在公网访问时配置一个密钥否则会提示格式被禁。3.2 搭建Agent调度核心把系统提示词当“员工手册”来写调度核心是整个项目的中枢神经也是我认为最值得讲的部分。很多人写Agent系统提示词非常随意随手写几句“你是一个有用的助手”就完事了。但Agent-Reach 的场景里有大量工具调用提示词必须写得像一份员工手册什么场景用什么工具、工具参数怎么填、回答时有什么约束、处理不了时该怎么办。我最终的系统提示词包含五个板块角色定位、能力清单、工具使用规则、输出格式规范、边界与免责。能力清单部分明确列出了可用的工具、每个工具的适用场景和不适用场景。举个例子搜索工具说明“适合理事实性信息查询不适合专业领域分析”RAG工具说明“直接回答知识库范围内的问题不要编造来源”浏览器工具说明“仅在确需实时操作页面时使用优先用搜索和抓取API”。提示词在精不在长我的版本大约1200字但每个句子都经过实际测试调整。有一版我写得太细致反而导致模型变得畏手畏脚很多简单问题都不直接回答了而是反复确认用户意图。后来砍掉了一半修饰性描述把“动作导向”的指令前置模型行为才恢复正常。这个经验值得分享给模型的指令不要追求面面俱到要留出推理空间核心是让模型知道工具是干什么的、边界在哪而不是规定每一步怎么做。SYSTEM_PROMPT 你是一个名为 Agent-Reach 的个人智能助理具备实时搜索、本地知识库检索和浏览器操作能力。 可用工具 1. web_search(query, time_range): 联网搜索公开信息用于获取实时资讯或事实校验。 2. rag_search(query, collection): 检索本地知识库用于回答私有文档相关问题。 3. browser_action(action, url, selector, value): 操作浏览器用于访问动态页面或提取结构化数据。 4. file_write(filename, content): 将结果写入指定目录的文件。 工具使用规则 - 搜索是首选的获取外部信息方式优先使用标题和摘要不要臆造数据。 - rag_search 结果代表已知事实回答时优先参考知识库未覆盖的内容明确说明知识库中未找到。 - browser_action 仅当搜索和抓取无法获取所需信息时使用。 - 每次调用必须给出具体合法的参数参数不明确时主动追问用户。 回答要求 - 简洁、结构清晰使用小标题和列表。 - 涉及时效性数据必须注明来源或截取时间。 - 信息不足时如实说明禁止编造。 调度循环的代码在上一节已经展示过这里再补充一个细节错误的处理。工具执行时可能异常超时、网络错误、参数解析失败不能直接把堆栈信息抛给模型而是要包装成标准化的错误对象返回比如{error: tool_timeout, message: web_search 调用超时请降低 query 复杂度后重试}。这样模型能理解发生了什么并自主决定是换一个参数重试、换一个工具还是直接向用户说明情况。3.3 定时任务与推送让Agent真正“主动”起来Agent只能被动回复对话那和普通聊天机器人没什么区别。Agent-Reach 的杀手锏之一就是定时任务每天早上8点自动执行“搜集今日行业动态和价格变化并生成简报”完事后把简报推送到我的企业微信Webhook和本地目录。APScheduler这个库用起来比较直观我用CronTrigger设置每天的触发时间点。触发后不是直接拼接模板而是真正跑一个Agent会话系统给它一个预置的“晨报生成”任务描述包含要搜索的关键词、要查询的知识库范围、输出格式要求。此后Agent会自行决定搜索几次、检索哪些文档、最后如何汇总成稿。定时任务里最怕的是中途卡死。比如某个搜索接口响应缓慢可能导致整个链路的等待时间超过预期。我的处理方式是给每个工具执行都加了硬超时默认20秒APScheduler的任务本身也设置了max_instances1和misfire_grace_time300防止任务重叠执行或错过触发窗口后瞬间补跑多次。推送部分我用了一个轻量方案直接把Webhook URL配置在环境变量里工具模块提供send_notification(title, content)方法。这个方法在定时任务里被Agent自主调用相当于Agent“自己决定”播报结果。这样做的好处是整个链路完全由Agent驱动不需要在定时任务脚本里硬编码推送逻辑后续如果要扩展到钉钉、飞书或者邮件只需要改这一个工具的实现。3.4 前端交互界面FastAPI接管一切虽然Agent核心是命令行可跑的但为了让操作更顺手、结果更直观我还是加了一个简单的Web界面。后端用FastAPI起了三个接口POST /api/chat发送消息并返回Agent最终回复、GET /api/history拉取历史会话、GET /api/files列出生成的文件。前端是单页HTML用原生JavaScript封装的聊天窗口支持显示Markdown渲染后的内容。设计Web界面时我特别注意了流式输出。大模型生成长答案时如果等全部生成完再一次性返回体验会很煎熬。我用了FastAPI的StreamingResponse把Agent循环中每次模型返回的增量token直接推送到前端前端SSE接收后实时渲染。这个改造不复杂但体验提升巨大强烈建议做。另一个界面细节是工具调用过程的可视化。用户发一句话后Agent可能背后默默调用了三个工具如果不展示这个过程用户会以为它在瞎编。我在前端把工具调用记录整理成一条时间线调用时间、工具名称、参数摘要、返回状态。这样整个思考过程是透明的出现问题也方便回溯。app.post(/api/chat) async def chat_endpoint(request: ChatRequest): async def event_generator(): async for chunk in run_agent_stream(request.message): yield fdata: {json.dumps(chunk, ensure_asciiFalse)}\n\n return StreamingResponse(event_generator(), media_typetext/event-stream)3.5 核心链路验证从一句话到一份完整报告为了验证这套系统真正跑得通我设计了一个端到端的测试案例。测试指令是“帮我查一下最近一周A股光伏板块的主要新闻、行业指数变化结合知识库里我存的两篇研报生成一份500字左右的周报保存到文件然后推送通知我。”这条指令覆盖了四条核心工具链路搜索资讯、检索知识库、生成报告文件、推送通知。实际运行中Agent先调用web_search搜索了三次光伏板块新闻、光伏指数、产业链动态然后对抽取出来的链接做了必要的网页抓取接着执行rag_search在两篇研报的向量集合里找相关段落最后根据所有信息生成Markdown格式的周报调用file_write存盘再调用send_notification推送。整个过程跑了大概60秒模型总共调用了6次工具。输出质量比我预想的要好新闻部分有来源有日期研报引用部分标注了哪篇文档整体结构也不像模板堆出来的。这个案例验证了事件驱动的工具编排确实可行——模型自己决定先搜索哪几个词、搜到结果后是否深入抓取、再决定是否引用本地知识库完全不需要用户在每一步做干预。4. 常见问题与排查技巧实录4.1 工具调用循环里的“上下文污染”问题调试过程中遇到频率最高的一个问题多轮工具调用后上下文里的工具返回内容越来越多把核心信息挤到了边缘导致模型后续决策质量急剧下降。尤其是搜索工具返回的原始JSON很大时几轮下来token用量翻倍回答速度也肉眼可见地变慢。解决办法有两层。第一层是做工具结果摘要在执行工具后、返回给模型前把内容压缩到关键点搜索只列出标题和简介网页抓取只保留正文关键段落。第二层是做记忆压缩当对话历史超过一定长度时把旧消息用模型做一轮概括形成“早前对话摘要”放在上下文顶部然后丢弃旧消息。这两招配合使用我单次会话的token消耗降低了差不多70%模型的长程决策稳定性也明显提升。4.2 Playwright自动化时的“登录态丢失”和元素定位漂移浏览器自动化的坑比想象中多。登录态丢失是最头疼的有些网站的会话有效期只有两三个小时第二天定时任务一起发现已经跳到登录页了。排查后发现有些站点不仅看Cookie还会校验浏览器的指纹和LocalStorage里存的标记单纯复用Cookie不够。我这里做了一个二次兜底如果检测到页面出现了登录特征元素就尝试读取配置里存储的用户名密码自动填充登录表单万一登录也有验证码就触发送通知提醒人工处理。元素定位漂移是另一个高频问题。网站的CSS class经过改版后经常会变头天写的选择器隔两天就失效了。对此我总结了一个相对可靠的经验优先用文本匹配和结构关系定位而不是死磕CSS class。比如要提取价格信息选择xpath//div[contains(class, price)]//span这类宽松表达同时把断言和等待条件写齐全元素没出现就视为失败而不是报错。4.3 模型“幻觉”来源搜索到但没搜准搜准了但没读懂Agent时代幻觉并没有消失只是转移了阵地。我遇到的情况主要是模型拿到了搜索结果的标题和摘要但没有真正打开页面核实内容就基于标题的下意识联想开始编答案。解决方式是明确告诉模型“搜索结果仅作为线索涉及具体数据必须通过页面抓取确认”并且在搜索工具返回时附带提示“本条结果未打开正文引用数据前请确认”。这个轻量的约束让模型在没有把握时主动去调用页面抓取工具而不是臆测。还有一种情况是RAG检索到的文档相关度不够模型却硬要往上靠把无关段落拼进回答里。我用相似度阈值和回答溯源来解决召回结果中如果最高相似度低于阈值工具返回时注明“相关性可能不足”让模型可以选择如实告诉用户“知识库中暂无高置信度内容”。这虽然让系统显得“没那么聪明”但从可信度角度看总线比一本正经胡说强得多。4.4 定时任务运行异常与日志追踪定时任务跑了几周后我开始意识到日志可观测性的重要性。Agent的每次工具调用、参数、耗时、返回码以及定时任务的触发时间、执行时长、是否成功都需要有完整的记录。我使用了loguru管理日志每个Agent轮次都会打印一份日志片段格式包括时间戳、消息ID、工具名称、调用参数、返回摘要。这样出现问题后扒日志比靠猜高效太多。这里分享一个实在的心得不要只在代码里埋日志点还要给日志加上轮转机制。如果不做轮转几个星期后日志文件可能膨胀到几GB拖累磁盘和排查效率。我设置每天轮转一次、保留14天既保证有足够的历史数据做回溯又不会把磁盘吃满。日志文件还接了grep友好的格式随手搜一下工具名就能定位问题区间。4.5 故障排查速查表现象可能原因排查路径解决方案模型完全不调用工具系统提示词未声明工具或工具Schema格式错误检查API请求是否包含tools参数打印模型原始响应修正工具Schema在提示词里示例一次正常调用工具返回值缺失执行时报错被静默捕获查看工具执行函数是否try/except吞掉异常移除静默捕获让异常向上抛出并记录日志搜索返回结果过时SearXNG的缓存或时间范围参数未生效请求里检查time_range字段是否传递显式设置搜索时间窗重启SearXNG容器定时任务没触发时区配置错误或任务被上一次运行阻塞检查APScheduler时区设置和max_instances把时区设为Asia/Shanghai确保任务无重叠运行回答长得离谱但缺乏重点上下文太长模型丢失核心约束统计发送给模型的token数和消息轮数启用历史压缩在提示词里强调输出长度限制浏览器操作被反爬拦截WebDriver特征暴露对比正常浏览器和自动化浏览器的navigator属性使用stealth补丁随机化鼠标轨迹和操作间隔最后两个值得动手试的小技巧根据我连续跑了二十多天的实际体验有两个小技巧可以说是性价比极高。第一个是在Agent循环里加一个“确认再执行”的开关——当模型判断工具调用可能产生不可逆影响比如写入文件、发送推送时先在界面上弹一层确认等用户点了再放行。这个开关代码量不大但能防住手误和模型理解偏差导致的误操作尤其是定时任务里挂了一堆自动化动作时多一道确认就像给工作流上了保险。第二个是给每个生成的报告文件加统一的元信息头包括生成时间、调用工具列表、数据来源链接。这样即使过了几个星期回看报告也能快速知道这份文件的信息源头是什么、是自动生成还是人工整理。这个习惯来自一次尴尬经历——我拿一份两周前的报告给朋友看结果说不清里面的数据来自哪里后来就给所有文件都加了来源标注。这个项目目前仍在小步迭代中。我自己在考虑的方向包括接入语音输入做纯语音交互、把定时任务的规则做成本地可写配置文件方便无代码调整、以及让知识库支持增量更新而不需要全量重建。Agent-Reach 的代码本身没有复杂到需要专门造框架它最大的价值是提供了一个透明的、可控的、能让你理解每个决定背后逻辑的智能体范式。如果这篇文章里的方案能给你一点启发可以试着从最小的工具调用循环开始搭起动手跑通一次完整的“搜索-分析-输出”链路后你会对Agent项目有完全不同的感觉。