资讯动态

没有 Shell、没有铸造凭据:freehire 如何在 API 进程内运行有界 Tool-Calling Agent 循环

发布时间:2026/10/22 17:49:23 来源:尧图企业网站定制
【免费下载链接】freehirefreehire — the open-source search engine for job seekers项目地址https://gitcode.com/GitHub_Trending/fr/freehire点击查看免费下载freehire 是一个开源的求职搜索引擎它把 AI Agent 直接跑在自己的 API 进程里模型想调用工具时工具拿到的不是 Shell也不是临时铸造的令牌credential而是会话属主的userID随后直接调用同一个 Go 服务。这种有界bounded的 Tool-Calling Agent 循环最多跑 8 轮无人值守的 CV 定制跑 30 轮每一步都有持久化、有取消、有明确终止事件是新手理解LLM Agent 安全落地的好样本。先认识 freehire不止是工作搜索引擎freehire 聚合了 300 万条直接抓取自公司 ATSWorkday、Greenhouse、Lever、Ashby 等 92 个平台的在招职位并提供分面搜索、CV 定制、申请追踪等功能。它的技术栈是 Go Fiber 的 HTTP 服务加上 PostgreSQL、Meilisearch 和任意 OpenAI 兼容的 LLM 端点通过 Bifrost 网关做按人计费。除了找工作freehire 还内置了一个应用内 AI 助手in-app assistant支持聊天、CV 定制、档案问答、面试演练等 5 种预设。今天的主角就是这个助手背后的 Agent 循环核心代码全部位于 internal/ai/assistant/ 包中。为什么 Agent 要跑在 API 进程里常见的 Agent 部署方式有两种外部独立运行时单独起个容器跑 Agent或给模型发一个可操作的凭据让它自由发挥。freehire 在 assistant 的 AGENTS.md 里把设计原则写得非常直白这个 Agent 运行在本进程内。没有外部运行时、没有 Shell、没有凭据——工具拿到的是会话属主的userID然后直接调用 Go 服务。工具不该到达的东西就是一个不存在的工具。这句话是理解整个架构的钥匙。工具拿到的是 userID不是令牌看 tool.go 中的工具契约type Tool struct { Name string Description string Schema map[string]any Run func(ctx context.Context, userID int64, args json.RawMessage) (any, error) }注意Run的签名它接收userID int64而不是任何 API token。这意味着没有需要铸造mint的凭据不用为每轮对话签发短期令牌也就没有令牌泄漏、续期、撤销这一整套问题权限收敛在数据库查询里工具内部调用的服务和 HTTP 接口用的是同一套服务代码归属校验就是一句WHERE user_id $1——不属于自己的会话直接按不存在404处理越权路径物理上不存在模型连用谁的权限访问都决定不了身份在循环启动时就被绑定死了。不该存在的工具干脆不注册工具的注册发生在 internal/api/handler/assistant_tools.go每个会话按其预设preset组装工具集。比如 CV 定制会话的cv_edit工具直接闭包绑定该会话的 CV id 和职位 id模型就算猜中别的 CV id 也写不进去邮件工具没有发送能力——因为邮件正文是攻击者可控文本最稳的防提示注入办法就是让注入没有出站通道。有界 Agent 循环8 轮上限是怎么定的Agent 循环的核心是 runner.go 中的Runner.Run流程可以概括为POST /api/v1/assistant/sessions/:id/messages └─ handler属主校验 → 按预设取注册表 → SSE 流 └─ Runner.Run ├─ 持久化用户消息发射 user_prompt 事件 ├─ 重建历史 系统提示 最近 N 条转录 └─ 循环≤ MaxSteps llm.Chat(历史, 工具) ──► 纯文本→ 发 result(end_turn)结束 └─► 工具调用→ 逐个执行、发射 tool_use/tool_result、持久化 └─ 触顶 → 最后一次无工具的 Chat → result(max_steps)循环的每一步都被持久化循环里产生的每一帧用户消息、模型文本、工具调用、工具结果都边生成边写入转录表。因此一个中途死掉的回合仍留下可续跑的会话重试走Runner.Continue它不会重复追加用户消息那会在模型上下文里复制一份请求只会从现有转录续跑。上限不是无限而是默认值NewRunner 对MaxSteps的处理很能说明工程态度零值或负值会回退到默认值 8 轮而不是解释为不设限。注释里写得很直白在按量计费的网关上跑一个不设限的 Agent 循环就是一张失控的账单。而单回合的 LLM 调用超时是第二道边界。需要说明的是这些上限只能由服务端选择——客户端能调高的上限就不叫上限。不同场景的上限也不同普通提问 8 轮、交互式 CV 定制 16 轮、无人值守的 autopilot 定制 30 轮一次遍历职位的全部要求。触顶后循环会再发最后一次不带工具的 Chat逼模型给出总结——即使某些供应商如 Bedrock 上的 Haiku此时仍返回工具调用代码也会先执行完它们再停止保证转录永远不会以一个悬空的tool_use结尾。工具失败 ≠ 回合失败这是新手最容易忽略的设计。Registry.Call从不返回 Go error未知工具、参数格式错误、服务调用失败统统包装成{error: ...}的 JSON 交还给模型阅读。错误信息会指名哪个值非法、合法值有哪些——因为这条消息是模型自我纠正的唯一路径。只有模型/传输层本身的失败才会让整个回合以error结束。配合 DecodeArgs 的严格解码拒绝未知字段、拒绝尾部多余内容一次写错的工具调用会得到可行动的失败而不是带着半个参数悄悄执行。每个回合都终结于恰好一个 result 事件如果客户端收到一条悄悄停掉的流它会永远等下去。所以 runner.go 定义了四种终止原因end_turn/max_steps/cancelled/error并保证所有退出路径——包括循环开始前就死掉的写库失败——都发射一个result事件。取消用户点 Stop是一个独立的POST /assistant/sessions/:id/cancel请求因为连接断了和用户主动停止无法区分而一次回合还会活过它的读者SSE 写失败只说明这个读者不再听了回合继续跑完、转录照常落库。Preset一份提示词 一组工具会话的预设chat、tailor、profile、browse、interview、debrief只决定两件事系统提示词和注册的工具集。词汇表由数据库assistant_sessions.preset列的 CHECK 约束钉死所以新增一个预设是模式变更而不仅仅是一个 Go 常量。预设的提示词由 SystemPrompt(preset, language) 生成且每份系统提示都携带一条 LANGUAGE 指令——按候选人档案里保存的语言作答而不是猜他这条消息用什么语言打的字。同一个 chat 组件因此能同时服务网页端的/my/assistant、CV 定制工作区、面试演练和浏览器扩展的侧边栏。转录即历史一张表两个用途message.go 里的Messageseqrole JSON 载荷同时是客户端回放的数据和重建模型历史的数据。两个用途共用一张表带来的约束很值得学习模型的原始参数字符串原样存储只做一处修复剥掉 Haiku 在合法 JSON 后追加的/invoke尾巴因为重新编码会改变模型看到的字节历史窗口按块滑动而不是每回合滑一条让前缀在块内保持逐字节一致从而命中供应商的提示词缓存——生产上曾测量到每请求 3 万 token 几乎全部按全价重读修好块对齐后成本大幅下降见 runner.go 中 historyBlock 的注释重建历史时会丢弃窗口开头孤立的 tool_result并为悬空的 tool_use合成一条错误结果——因为 Bedrock 等供应商会拒绝整条不合法的请求孤儿消息会把丢上下文升级成整个回合失败。花费可控按人计费 按天配额Agent 每回合都走调用者自己的网关凭据由 internal/ai/llmkey/ 在其首次 AI 调用时铸造Runner 在启动时构建一次、按回合克隆Runner.With因为凭据是按人的而边界不是。每次调用带feature:assistant和preset:preset两个标签网关按标签记一行消费——这样一次面试演练和一次无人值守定制的花费就可比较了。真正拒绝一个回合的是 internal/ai/plan/ 的配额按功能、按 UTC 天计额的允许量超额在流打开前就返回 HTTP 402。而网关侧的账户级预算LLM_USER_MAX_BUDGET故意留空——在消费分布未知前设定的天花板只是猜测配额是给产品定价的网关预算才是保险丝。关键文件索引文件作用internal/ai/assistant/runner.goAgent 循环、事件形状、边界与持久化internal/ai/assistant/tool.go工具契约、注册表、严格参数解码internal/ai/assistant/message.go转录消息编码与往返internal/ai/assistant/prompt.go各预设的系统提示词internal/api/handler/assistant_tools.go按预设组装工具注册表internal/ai/assistant/AGENTS.md该模块最完整的设计说明强烈推荐细读小结freehire 的助手 Agent 证明了把 LLM Agent 放进 API 进程内并不需要牺牲安全性去换便利。它的三板斧是——身份用 userID 绑定而非凭据、权限用工具不存在实现而非运行时检查、循环用步数与超时双重封顶并全程持久化。对于想给自己的 Go 服务加 AI Agent 的开发者来说这套约一千行的assistant包是一个可以直接对照着学的有界 Tool-Calling 循环参考实现。赞分享【免费下载链接】freehirefreehire — the open-source search engine for job seekers项目地址https://gitcode.com/GitHub_Trending/fr/freehire点击查看免费下载相关推荐开源职位搜索引擎 freehire 深度解析330万条在招职位直连企业 ATS没有招聘中介、没有重发、没有死链开源职位搜索引擎 freehire 深度解析330万条在招职位直连企业 ATS没有招聘中介、没有重发、没有死链 freehire 是一个 开源职位搜索引擎JavaScript 教程实战for 循环中 i 与 i 到底有没有区别JavaScript 教程实战for 循环中 i 与 i 到底有没有区别 本篇基于现代 JavaScript 教程zh.javascript.in文档教程Jev 到底有没有被高估Jev 到底有没有被高估 2026 年 9 月 15 日前 OpenAI InstructGPT 核心作者 Diogo Almeida 创立的 TypeSafAI 应用大模型交互助手RAG上一篇为什么Q8量化可能改变说话人边界Nemotron-3-Diarization-GGUF输出一致性风险与应对清单下一篇让AI写中文海报翻车记复旦MOSS-Image-SFT三大坑我替你踩了个遍创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑