【转】构建有效的智能体

什么是智能体

“智能体”(Agent)可以有多种定义方式。一些客户将智能体定义为完全自主的系统,能够长时间独立运行,并利用各种工具完成复杂任务。另一些客户则用该术语指代遵循预设流程、更具规范性的实现方式。在 Anthropic,我们将所有这些变体统称为智能体系统(agentic systems),但在架构上对工作流(workflows)和智能体(agents)做出了重要区分:

  • 工作流(Workflows)是指通过预定义的代码路径来编排 LLM(大语言模型)和工具的系统。
  • 智能体(Agents)则是指由 LLM 动态引导自身流程和工具使用的系统,保持对任务完成方式的控制权

接下来,我们将详细探讨这两种类型的智能体系统。在附录 1(“智能体实践”)中,我们将具体描述客户在应用此类系统时发现特别有价值的两个领域。

何时(何时不)使用智能体

在构建基于大语言模型(LLM)的应用时,我们建议尽可能寻求最简单的解决方案,仅在必要时才增加复杂性。这可能意味着完全不构建智能体系统。智能体系统通常以延迟和成本为代价,来换取更好的任务表现;您需要仔细考量这种取舍在何时是合理的。

当确实需要更高的复杂性时,工作流(Workflows)为定义明确的任务提供了可预测性和一致性;而当需要大规模灵活性和模型驱动的决策时,智能体(Agents)是更好的选择。然而,对于许多应用而言,通过检索(Retrieval)和上下文示例(In-context Examples)来优化单次 LLM 调用通常就足够了

何时以及如何使用框架

目前已有许多框架能够简化智能体系统的实现,包括:

  • LangGraph(来自 LangChain);
  • Amazon Bedrock 的 AI Agent 框架
  • Rivet(一款拖拽式图形界面 LLM 工作流构建工具);以及
  • Vellum(另一款用于构建和测试复杂工作流的图形界面工具)。

这些框架通过简化诸如调用 LLM、定义和解析工具、链式调用等标准的底层任务,使得入门变得容易。然而,它们通常也会引入额外的抽象层,这些抽象层可能掩盖底层的提示词和响应细节,使其更难以调试。此外,当更简单的设置就足够时,这些框架也可能诱使开发者添加不必要的复杂性。

我们建议开发者从直接使用 LLM API 开始:许多模式只需几行代码即可实现。如果确实需要使用框架,请确保理解其底层代码。对框架内部机制的错误假设是客户遇到问题的常见根源。

有关一些示例实现,请参阅我们的开发指南

构建单元、工作流程和智能体

在本节中,我们将探讨在生产环境中常见的智能体系统模式。我们将从基础构建单元——增强型LLM(Augmented LLM)——开始,并逐步提升复杂度,从简单的组合式工作流(Compositional Workflows) 到自主智能体(Autonomous Agents)

构建单元:增强型LLM (Augmented LLM)

智能体系统的基本构建单元是经过增强的大语言模型(LLM),其增强功能包括检索(retrieval)、工具(tools)和记忆(memory)。我们当前的模型能够主动调用这些能力——自主生成搜索查询、选择合适的工具,并决定应留存哪些信息

在实现方面,我们建议重点关注两个关键方面:针对您的特定用例定制这些能力,以及确保它们为您的LLM提供一个简单、文档完善的接口。虽然实现这些增强功能的方法多种多样,但其中一种方式是通过我们最近发布的 Model Context Protocol。该协议允许开发者通过简单的客户端实现,与日益增长的第三方工具生态体系进行集成。

在本文的后续部分,我们将默认每次LLM调用都具备这些增强能力

工作流:提示链(Prompt Chaining)

提示链(Prompt Chaining) 将一个任务分解为一系列步骤,其中每次LLM调用都处理前一次调用的输出。您可以在任何中间步骤添加程序化检查(参见下图中的“门控(gate)”),以确保流程仍在正轨上。

何时使用此工作流: 此工作流非常适用于任务能够轻松、清晰地分解为固定子任务的情况。其主要目标是以延迟为代价换取更高的准确性,因为每个LLM调用处理的任务都变得更简单。

提示链有用的示例:

  1. 生成营销文案(Marketing copy),然后将其翻译成另一种语言。
  2. 撰写文档大纲,检查该大纲是否符合特定标准,然后根据大纲撰写文档。

工作流:路由 (Routing)

路由(Routing) 对输入进行分类,并将其导向专门的后续任务。这种工作流实现了关注点分离(separation of concerns),并允许构建更专业化的提示词。若不采用此工作流,针对某一类输入的优化可能会损害其他输入的处理效果。

何时使用此工作流: 路由非常适用于处理复杂任务的情况,特别是当任务中存在明显不同的类别(这些类别更适合分开处理),并且分类过程能够准确完成(无论是通过LLM还是更传统的分类模型/算法)时。

路由有用的示例:

  1. 将不同类型的客服查询(如:一般性问题、退款请求、技术支持)引导至不同的下游流程、提示词和工具进行处理。
  2. 将简单/常见的问题路由到较小的模型(如 Claude 3.5 Haiku),将困难/罕见的问题路由到能力更强的模型(如 Claude 3.5 Sonnet),以优化成本和速度。

工作流:并行化 (Parallelization)

大语言模型(LLM)有时可以同时处理一个任务,并通过程序化方式聚合它们的输出。这种工作流,即并行化(Parallelization),主要有两种关键形式:

  1. 分块执行(Sectioning):将任务分解为独立的子任务并行运行
  2. 投票聚合(Voting)多次运行相同的任务以获得多样化的输出。

何时使用此工作流: 当被划分的子任务可以并行执行以提高速度,或者需要多种视角或多次尝试以获得更高置信度的结果时,并行化非常有效。对于具有多重考量因素的复杂任务,LLM通常在每个考量因素由单独的LLM调用处理时表现更好,这允许模型专注于每个特定的方面。

并行化有用的示例:

  • 分块执行(Sectioning)示例:

    • 实施防护机制(Guardrails):一个模型实例处理用户查询,同时另一个模型实例对查询进行不当内容或请求筛查。这通常比让同一个LLM调用既处理防护又处理核心响应的效果更好
    • 自动化评估(Automating evals):评估LLM在给定提示词上的性能,其中每个LLM调用评估模型性能的不同方面(例如:相关性、安全性、流畅度等)。
  • 投票聚合(Voting)示例:

    • 审查代码漏洞:使用多个不同的提示词审查同一段代码,如果任何一个提示词发现问题就标记该代码。
    • 评估内容是否不当:使用多个提示词评估内容的不同方面(如:仇恨言论、暴力、色情),或者要求达到不同的投票阈值(vote thresholds) 以平衡假阳性(false positives)和假阴性(false negatives)

工作流:协调者-工作者 (Orchestrator-workers)

协调者-工作者(Orchestrator-Workers)工作流中,一个中央LLM(协调者) 会动态地分解任务,将它们委派给工作者LLMs,并合成它们的结果

何时使用此工作流: 此工作流非常适合处理无法预测所需子任务的复杂任务(例如,在编码任务中,需要修改的文件数量以及每个文件的修改性质很可能取决于具体任务)。虽然结构上与并行化(Parallelization)相似,但其关键区别在于灵活性——子任务并非预先定义,而是由协调者根据具体输入动态确定

协调者-工作者工作流有用的示例:

  1. 编码类产品:每次都需要对多个文件进行复杂修改的编码任务。
  2. 搜索任务:需要从多个来源收集并分析信息以寻找可能相关信息的搜索任务。

工作流:评估者-优化者 (Evaluator-optimizer)

评估者-优化者(Evaluator-Optimizer)工作流中,一个LLM调用负责生成响应,而另一个LLM调用则在一个循环中提供评估和反馈

何时使用此工作流: 当我们拥有清晰的评估标准,并且迭代优化能带来可衡量的价值时,此工作流特别有效。适用该工作流的两个关键信号是:首先,当人类明确表达反馈时,LLM的响应能够得到显著改进其次,LLM本身能够提供此类反馈。这类似于人类作者在创作一篇经过润色的文档(polished document) 时可能经历的迭代写作过程。

评估者-优化者工作流有用的示例:

  1. 文学翻译(Literary translation):涉及语言微妙差异(nuances) 的场景,翻译LLM可能无法在初次尝试时就完全把握,但评估者LLM能够提供有价值的批判性反馈(如:风格、文化内涵、韵律)。
  2. 复杂搜索任务:需要多轮搜索和分析才能收集全面信息的任务,其中评估者LLM负责判断是否需要进一步搜索

智能体

随着大语言模型(LLM)在关键能力上的成熟——包括理解复杂输入、进行推理和规划、可靠地使用工具以及从错误中恢复——智能体(Agents) 正在生产环境中崭露头角。

智能体开始工作时,会接收人类用户的指令或与其进行交互讨论。一旦任务明确,智能体便独立进行规划和操作,并可能在需要时返回人类处寻求更多信息或判断。在执行过程中,智能体在每个步骤都从环境中获取“真实反馈”(ground truth)(例如工具调用结果或代码执行结果)以评估其进展。智能体可以在检查点(checkpoints) 或遇到阻碍(blockers) 时暂停,等待人类反馈。任务通常在完成后终止,但也常会设置停止条件(stopping conditions)(例如最大迭代次数)以保持控制。

智能体能够处理复杂的任务,但其实现通常很直接。它们通常只是一个循环运行的LLM,基于环境反馈使用工具。因此,清晰且深思熟虑地设计工具集及其文档至关重要。我们在附录 2(“工具提示词工程(Prompt Engineering your Tools)”)中详细阐述了工具开发的最佳实践。

何时使用智能体: 智能体适用于处理开放式问题(open-ended problems),这类问题的所需步骤数量难以或无法预测,并且无法硬编码固定路径。LLM可能需要运行多个轮次,因此您必须对其决策能力有一定程度的信任。智能体的自主性(autonomy) 使其成为在受信任环境中扩展任务的理想选择。

智能体的自主特性也意味着更高的成本错误累积(compounding errors) 的可能性。我们建议在沙盒环境(sandboxed environments) 中进行广泛的测试,并设置适当的防护机制(guardrails)

智能体有用的示例:

以下示例来自我们自己的实现:

  1. 一个用于解决 SWE-bench 任务的编码智能体:这些任务涉及根据任务描述修改多个文件
  2. 我们的 “计算机操作”(“computer use”)参考实现:在该实现中,Claude 使用计算机来完成各种任务

组合与定制这些模式

这些构建单元(building blocks) 并非规定性的(prescriptive)。它们是开发者可以塑造(shape)和组合(combine) 以适应不同用例的常见模式(common patterns)。与任何LLM功能一样,成功的关键在于衡量性能(measuring performance)并对实现进行迭代(iterating on implementations)。再次强调:仅当能够显著改善结果时,您才应考虑增加复杂性。

总结(Summary)

在LLM领域取得成功,不在于构建最复杂的系统,而在于构建适合您需求的正确系统从简单的提示词开始,通过全面评估(comprehensive evaluation) 对其进行优化,仅当更简单的解决方案无法满足需求时,才添加多步骤的智能体系统。

在实现智能体时,我们力求遵循三个核心原则:

  1. 保持智能体设计的简洁性(Maintain simplicity)
  2. 优先保证透明度(Prioritize transparency)明确展示智能体的规划步骤
  3. 精心设计智能体-计算机接口(agent-computer interface, ACI):通过详尽的工具文档和测试来实现。

框架可以帮助您快速入门,但在进入生产环境时,请毫不犹豫地减少(甚至移除)抽象层,并使用基础组件进行构建。遵循这些原则,您可以创建出不仅强大,而且可靠、易维护且值得用户信赖的智能体。

附录1:智能体实践(Agents in practice)

我们与客户合作的经验揭示了两种特别有前景的AI智能体应用,它们展示了上述模式的实际价值。这两种应用都说明了智能体在哪些任务中能发挥最大价值:需要对话与行动相结合、具有清晰的成功标准、支持反馈循环、并整合了有意义的人工监督的任务。

A. 客户支持(Customer support)

客户支持将熟悉的聊天机器人界面通过工具集成实现的增强功能相结合。这自然更适合开放式智能体,因为:

  • 支持交互天然遵循对话流程,同时需要访问外部信息和执行操作;
  • 可以集成工具来拉取客户数据、订单历史记录和知识库文章
  • 诸如发放退款或更新工单等操作可以通过编程方式处理;
  • 成功可以通过用户定义的解决结果进行明确衡量。

多家公司已通过按成功解决量计费(usage-based pricing models) 的模式证明了这种方法的可行性,这显示了他们对其智能体有效性的信心。

B. 编码智能体(Coding agents)

软件开发领域在利用LLM功能方面展现出显著潜力,其能力已从代码补全发展到自主解决问题。智能体在此特别有效,因为:

  • 代码解决方案可以通过自动化测试进行验证
  • 智能体可以利用测试结果作为反馈来迭代解决方案
  • 问题空间是定义明确且结构化的
  • 输出质量可以客观地衡量

在我们自己的实现中,智能体现在仅基于拉取请求(pull request)描述,就能在 SWE-bench Verified 基准测试中解决真实的 GitHub issues(问题)。然而,虽然自动化测试有助于验证功能,但人工审查对于确保解决方案符合更广泛的系统要求仍然至关重要。

附录2:工具提示词工程(Prompt engineering your tools)

无论您构建哪种智能体系统,工具(tools) 都可能是您智能体的重要组成部分。工具使 Claude 能够通过在我们的 API 中精确指定其结构和定义来与外部服务和 API 交互。当 Claude 响应时,如果它计划调用工具,它将在 API 响应中包含一个 tool_use 块工具定义和规范的提示词工程应与您的整体提示词受到同等重视。在这个简短的附录中,我们将描述如何对您的工具进行提示词工程。

通常有多种方法可以指定相同的操作。例如,您可以通过编写差异(diff) 或重写整个文件来指定文件编辑。对于结构化输出,您可以在 Markdown 中或 JSON 内部返回代码。在软件工程中,这些差异是表面性的,可以无损地从一种格式转换为另一种格式。然而,某些格式对于 LLM 来说比其他格式更难编写

  • 编写差异(diff)需要知道在编写新代码之前,块头(chunk header)中有多少行正在更改。
  • 在 JSON 中编写代码(与 Markdown 相比)需要对换行符和引号进行额外转义

我们关于决定工具格式的建议如下:

  1. 给予模型足够的 token 来“思考”,以免它在编写时陷入困境。
  2. 使格式尽可能接近模型在互联网文本中自然看到的内容
  3. 确保没有格式化的“开销”,例如必须准确计算数千行代码的行数,或者对其编写的任何代码进行字符串转义。

一个经验法则是:思考投入在人类-计算机交互界面(HCI) 上的工作量,并计划投入同样多的精力来创建良好的智能体-计算机接口(ACI)。以下是一些关于如何做到这一点的想法:

  • 站在模型的角度思考(Put yourself in the model’s shoes):根据描述和参数,工具的使用方法是否显而易见?还是需要仔细思考?如果是后者,那么模型很可能也会遇到困难。一个好的工具定义通常包括使用示例、边界情况(edge cases)、输入格式要求以及与其他工具的清晰界限
  • 优化参数名称和描述(Optimize parameter names and descriptions):如何更改参数名称或描述以使事情更加清晰?可以将其想象为给团队中的初级开发者(junior developer)编写一份优秀的文档字符串(docstring)。在使用许多相似工具时,这一点尤其重要。
  • 测试模型如何使用您的工具(Test tool usage):在 Workbench 中运行许多示例输入,观察模型会犯哪些错误,并进行迭代。
  • 为工具设计防错机制(Poka-yoke your tools):更改参数,使其更难出错。

在构建用于 SWE-bench 的智能体时,我们实际上在优化工具上花费的时间比优化整体提示词还要多。例如,我们发现当智能体移出根目录后,模型在使用涉及相对文件路径(relative filepaths) 的工具时会出错。为了解决这个问题,我们将工具更改为始终要求绝对文件路径(absolute filepaths)——结果发现模型能够完美地使用这种方法。

参考:

Building Effective AI Agents \ Anthropic

『深度好文』来自Claude官方的AI Agent详解!

Laravel loadMissing()

Laravel 的 loadMissing 方法提供了一种灵活的方式,对现有模型或集合进行预加载。该方式避免了 N+1 的查询问题,同时允许你只在需要时加载关联。

当使用可选的内容或仪表板构建 API 时,此功能尤其有价值,因为不同的部分需要不同的关联数据。

1
2
3
4
5
$post->loadMissing(['comments', 'author']);
// With constraints
$post->loadMissing(['comments' => function($query) {
$query->latest()->take(5);
}]);

以下是仪表盘数据加载器的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php

namespace App\Http\Controllers;

use App\Models\Dashboard;
use Illuminate\Http\Request;

class DashboardController extends Controller
{
public function show(Request $request, Dashboard $dashboard)
{
// Load base relationships
$dashboard->loadMissing([
'widgets',
'owner',
]);

// Conditionally load additional data
if ($request->section === 'analytics') {
$dashboard->loadMissing([
'widgets.viewHistory' => function($query) {
$query->whereBetween('viewed_at', [
now()->subDays(30),
now()
]);
},
'widgets.interactions'
]);
}

if ($request->section === 'sharing') {
$dashboard->loadMissing([
'sharedUsers',
'shareLinks' => function($query) {
$query->where('expires_at', '>', now());
}
]);
}
return $dashboard;
}
}

loadMissing 方法智能地仅加载所需的关联:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// GET /dashboard/1?section=analytics
{
"id": 1,
"name": "Sales Overview",
"widgets": [
{
"id": 1,
"type": "chart",
"viewHistory": [
{
"viewed_at": "2024-02-01T10:30:00Z",
"views": 150
}
],
"interactions": [
{
"type": "filter_change",
"timestamp": "2024-02-01T11:20:00Z"
}
]
}
]
}

LoadMissing 提供了一个有效的方式来管理管理加载、优化数据库查询,同时保留了代码的灵活性。

Laravel 动态关联加载

Laravel 中的智能路由检测

Laravel 的 named 方法提供了一种干净的方法来确定当前请求是否与特定路由名称匹配。这个强大的功能允许你根据当前路由执行条件逻辑,非常适合分析、导航突出显示或权限检查。

当构建需要根据当前路由表现不同的组件时,这种方法变得特别有价值,而无需在整个应用中编写重复的条件检查。

1
2
3
if ($request->route()->named('dashboard')) {
// We're on the dashboard
}

以下是实现动态导航状态的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php

namespace App\View\Components;

use Illuminate\View\Component;
use Illuminate\Http\Request;

class NavigationMenu extends Component
{
public function __construct(private Request $request)
{
}

public function isActive(string $routeName): bool
{
return $this->request->route()->named($routeName);
}

public function isActiveSection(string $section): bool
{
return $this->request->route()->named("$section.*");
}

public function render()
{
return view('components.navigation-menu', [
'sections' => [
'dashboard' => [
'label' => 'Dashboard',
'route' => 'dashboard',
'active' => $this->isActive('dashboard')
],
'posts' => [
'label' => 'Blog Posts',
'route' => 'posts.index',
'active' => $this->isActiveSection('posts')
],
'settings' => [
'label' => 'Settings',
'route' => 'settings.index',
'active' => $this->isActiveSection('settings')
]
]
]);
}
}

在应用中使用时,该导航组件自动检测当前路由并进行相应的更新:

1
2
3
4
5
6
7
8
9

<nav>
@foreach($sections as $key => $section)
<a href="{{ route($section['route']) }}"
@class(['nav-link', 'active' => $section['active']])>
{{ $section['label'] }}
</a>
@endforeach
</nav>

named 方法简化了基于路由的逻辑,使得代码更具可维护性,并减少路由依赖的复杂性。

Laravel 中的智能路由检测

Laravel Easy Metrics

轻松创建应用程序的指标。(Easily create metrics for your application.)

https://github.com/sakanjo/laravel-easy-metrics

支持的指标

  • Bar metric
  • Doughnut metric
  • Line metric
  • Pie metric
  • Polar metric
  • Trend metric
  • Value metric

安装

1
composer require sakanjo/laravel-easy-metrics

使用

Value metric

1
2
3
4
5
use SaKanjo\EasyMetrics\Metrics\Value;
use App\Models\User;

$data = Value::make(User::class)
->count();

Query types

The currently supported aggregate functions to calculate a given column compared to the previous time interval / range

Min
1
2
Value::make(User::class)
->min('age');
Max
1
2
Value::make(User::class)
->max('age');
Sum
1
2
Value::make(User::class)
->sum('age');
Average
1
2
Value::make(User::class)
->average('age');
Count
1
2
Value::make(User::class)
->count();

Doughnut metric

1
2
3
4
5
6
7
use SaKanjo\EasyMetrics\Metrics\Doughnut;
use App\Models\User;
use App\Enums\Gender;

[$labels, $data] = Doughnut::make(User::class)
->options(Gender::class)
->count('gender');

It’s always better to use the options method even though it’s optional, since the retrieved data may not include all enum options.

Query types

The currently supported aggregate functions to calculate a given column compared to the previous time interval / range

Min
1
2
Doughnut::make(User::class)
->min('age', 'gender');
Max
1
2
Doughnut::make(User::class)
->max('age', 'gender');
Sum
1
2
Doughnut::make(User::class)
->sum('age', 'gender');
Average
1
2
Doughnut::make(User::class)
->average('age', 'gender');
Count
1
2
Doughnut::make(User::class)
->count('gender');

Trend metric

1
2
3
4
5
use SaKanjo\EasyMetrics\Metrics\Trend;
use App\Models\User;

[$labels, $data] = Trend::make(User::class)
->countByMonths();

Query types

The currently supported aggregate functions to calculate a given column compared to the previous time interval / range

Min
1
2
3
4
5
6
$trend->minByYears('age'); 
$trend->minByMonths('age');
$trend->minByWeeks('age');
$trend->minByDays('age');
$trend->minByHours('age');
$trend->minByMinutes('age');
Max
1
2
3
4
5
6
$trend->maxByYears('age'); 
$trend->maxByMonths('age');
$trend->maxByWeeks('age');
$trend->maxByDays('age');
$trend->maxByHours('age');
$trend->maxByMinutes('age');
Sum
1
2
3
4
5
6
$trend->sumByYears('age'); 
$trend->sumByMonths('age');
$trend->sumByWeeks('age');
$trend->sumByDays('age');
$trend->sumByHours('age');
$trend->sumByMinutes('age');
Average
1
2
3
4
5
6
$trend->averageByYears('age'); 
$trend->averageByMonths('age');
$trend->averageByWeeks('age');
$trend->averageByDays('age');
$trend->averageByHours('age');
$trend->averageByMinutes('age');
Count
1
2
3
4
5
6
$trend->countByYears(); 
$trend->countByMonths();
$trend->countByWeeks();
$trend->countByDays();
$trend->countByHours();
$trend->countByMinutes();

Other metrics

  • Bar extends Trend
  • Line extends Trend
  • Doughnut extends Pie
  • Polar extends Pie

Ranges

Every metric class contains a ranges method, that will determine the range of the results based on it’s date column.

1
2
3
4
5
6
7
8
9
10
use SaKanjo\EasyMetrics\Metrics\Trend;
use SaKanjo\EasyMetrics\Metrics\Enums\Range;
use App\Models\User;

Value::make(User::class)
->range(30)
->ranges([
15, 30, 365,
Range::TODAY, // Or 'TODAY'
]);

Available custom ranges

  • Range::TODAY
  • Range::YESTERDAY
  • Range::MTD
  • Range::QTD
  • Range::YTD
  • Range::ALL

🔥 Practical examples

Filamentphp v3 widgets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php

namespace App\Filament\Widgets\Admin;

use App\Models\User;
use Filament\Widgets\ChartWidget;
use SaKanjo\EasyMetrics\Metrics\Trend;

class UsersCountChart extends ChartWidget
{
protected static ?string $heading = 'Users count trend';

protected function getData(): array
{
[$labels, $data] = Trend::make(User::class)
->range($this->filter)
->rangesFromOptions($this->getFilters())
->countByMonths();

return [
'datasets' => [
[
'label' => 'Users',
'data' => $data,
],
],
'labels' => $labels,
];
}

protected function getType(): string
{
return 'line';
}

protected function getFilters(): ?array
{
return [
15 => '15 Months',
30 => '30 Months',
60 => '60 Months',
];
}
}

hefengbao.github.io 无法加载样式

https://github.com/hefengbao/hefengbao.github.io 仓库使用 vitepress 建立站点,部署到 Github Page 后,访问 https://hefengbao.github.io 不能加载样式,,因为使用 vitepress 建立过 https://hefengbao.github.io/knowledge 等站点,所以配置是没问题的,做了一些测试,发现无法加载 assets 目录下的文件,然而 https://hefengbao.github.io/knowledge 站点下却是没问题的😓。

于是想有没有办法修改打包后静态资源输出的目录名称,查找 vitepress 的文档,果然可以设置:https://vitepress.dev/zh/reference/site-config#assetsdir。

1
2
3
export default {
assetsDir: 'static'
}

Windows 安装 Node.js

官方下载 Node.js — Download Node.js®

可以自定义安装路径,比如我安装在 E:\Program\nodejs 目录下,其他点“下一步/Next” 即可。

运行 npm -v 出错😓:

1
2
3
4
5
6
7
8
PS C:\Users\OoO> npm -v
npm : 无法加载文件 E:\Program\nodejs\npm.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microsoft.com/
fwlink/?LinkID=135170 中的 about_Execution_Policies。
所在位置 行:1 字符: 1
+ npm -v
+ ~~~
+ CategoryInfo : SecurityError: (:) [],PSSecurityException
+ FullyQualifiedErrorId : UnauthorizedAccess

解决:

请使用 以管理员身份运行 选项启动 PowerShell,运行如下命令🔗

1
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

npm config get prefix 命令查看全局依赖(inpm insall -g xx)安装位置,npm config get cache 查看 npm 缓存位置,可以看出都在 C 盘目录下。

1
2
3
4
PS C:\WINDOWS\system32> npm config get prefix
C:\Users\OoO\AppData\Roaming\npm
PS C:\WINDOWS\system32> npm config get cache
C:\Users\OoO\AppData\Local\npm-cache

如果 C 盘空间小,可以修改到其他盘。我的目录 E:\AppData\nodejs:

通过如下命令修改目录:

1
npm config set cache "E:\AppData\nodejs\node_cache"
1
npm config set prefix "E:\AppData\nodejs\node_global"

保存在C:\Users\{用户名} 目录下的 .npmrc 文件:


查看 .npmrc 文件:

1
2
cache=E:\AppData\nodejs\node_cache
prefix=E:\AppData\nodejs\node_global

修改环境变量,以下打开“环境变量”是基于 Windows 11 演示:

Laravel 请求流式(Stream)接口返回流式Http 响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<?php
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;


Route::get('http', function (){
return response()->eventStream(function (){
$stream = Http::withHeaders([
'Authorization' => 'Bearer '.env('DEEPSEEK_API_KEY'),
'Accept' => 'application/json',
])
->throw()
//->accept('application/x-ndjson')
->asJson()
->timeout(2000)
->withOptions(['stream' => true])
->post(env('DEEPSEEK_API_BASE_URL').'/chat/completions',[
'messages' => [
[
'content' => 'Hello',
'role' => 'system',
],
[
'content' => '西南财经大学',
'role' => 'user',
]
],
'model' => 'deepseek_r1.deepseek-r1', //deepseek_r1.deepseek-r1
'frequency_penalty' => 0,
'max_tokens' => 2048,
'presence_penalty' => 0,
'response_format' => [
'type' => 'text'
],
'stream' => true
])
->toPsrResponse()
->getBody();

$buffer = '';
while (!$stream->eof()) {
$char = $stream->read(1);

$buffer .= $char;
if ($char === "\n") {
$line = trim($buffer);
$buffer = '';

if (!empty($line)) {
yield $line;
}
}
}
});
});


Route::get('guzzle', function (){
$client = new \GuzzleHttp\Client([
'base_uri' => config('swufe-chat.base_url')
]);

return response()->eventStream(function () use ($client){
$response = $client->request('POST', '/api/chat/completions', [
'headers' => [
'Authorization' => 'Bearer '.env('DEEPSEEK_API_KEY'),
'Accept' => 'application/json',
],
'json' => [
'messages' => [
[
'content' => 'Hello',
'role' => 'system',
],
[
'content' => '西南财经大学',
'role' => 'user',
]
],
'model' => 'deepseek_r1.deepseek-r1', //deepseek_r1.deepseek-r1
'frequency_penalty' => 0,
'max_tokens' => 2048,
'presence_penalty' => 0,
'response_format' => [
'type' => 'text'
],
'stream' => true
],
'stream' => true
]);

$stream = $response->getBody();
$buffer = '';
while (!$stream->eof()) {
$char = $stream->read(1);

$buffer .= $char;
if ($char === "\n") {
$line = trim($buffer);
$buffer = '';

if (!empty($line)) {
yield $line;
}
}
}
});
});


// https://github.com/openai-php/client

Route::get('openai', function (){
$client = OpenAI::factory()
->withApiKey(env('DEEPSEEK_API_KEY'))
->withBaseUri(env('DEEPSEEK_API_BASE_URL'))
->withHttpClient($httpClient = new \GuzzleHttp\Client())
->withStreamHandler(fn (RequestInterface $request): ResponseInterface => $httpClient->send($request, [
'stream' => true // Allows to provide a custom stream handler for the http client.
]))
->make();

return response()->eventStream(function () use ($client){
$stream = $client->chat()->createStreamed([
'model' => 'deepseek_r1.deepseek-r1',
'messages' => [
['role' => 'user', 'content' => 'Hello!'],
],
'stream_options'=>[
'include_usage' => true,
]
]);

foreach ($stream as $response) {
yield $response->choices[0];
}
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$stream = Http::throw()
->accept('application/x-ndjson')
->asJson()
->withOptions(['stream' => true])
->post($endpoint, $requestData)
->toPsrResponse()
->getBody();

$buffer = '';

while ($stream->eof() === false) {
$char = $stream->read(1);

$buffer .= $char;
if ($char === "\n") {
$line = trim($buffer);
$buffer = '';

if (!empty($line)) {
dump($line);
}
}
}

参考:

https://yellowduck.be/posts/using-streaming-http-responses-in-laravel

官方也有实现😓

https://laravel.com/docs/12.x/responses#streamed-json-responses

Android Studio 修改 .android 和 .gradle 目录

Windows 系统安装 Android Studio 后,.gradle 用来保存下载的 Gradle 依赖的包,.android 用来保存模拟器,都比较占地方。默认位置为 C:\Users\<用户名>\.gradleC:\Users\<用户名>\.android ,如果 C 盘空间不够用,可考虑把这两个目录移到其他盘。首先把这两个目录移到其他盘,我这里移到了 D:\Program\AndroidDev 目录下。

添加环境变量,.gradle 需要添加的是 GRADLE_USER_HOME :

.android 需要添加的是 ANDROID_SDK_HOME :

Docker 容器中运行 Nginx

前置条件

  1. 操作系统已安装 Docker
  2. 有 root 账户或者可以运行 sudo 命令的账户

安装设置

1. 拉取镜像

1
sudo docker pull nginx

2. 运行 Ngnix 的 Docker 容器

1
sudo docker run -d -p 80:80 --name my-nginx-server nginx

-d: 以分离模式启动容器(容器在后台运行)。

-p: 绑定容器到主机的端口(将主机 8 0端口的请求导到容器的 80 端口)。

-name: Docker 容器的名字(实例中是my-nginx-server)

现在可以打开浏览器,使用本机 IP 访问:如 http://127.0.0.1。应该能看到 Nginx 的默认页面。

3. 查看 Docker 容器列表

1
sudo docker container ls

根据上图中的输出,可以使用两种方法停止 Nginx 的 Docker 容器:

1
2
sudo docker stop 19411f8b3f35
sudo docker stop my-nginx-server

查看所有的容器,包含未运行的,需要使用 -a 参数:

1
sudo docker container ls -a

4. 容器和主机之间共享数据

默认情况下,在容器内创建的任何数据只能在容器内使用,并且只能在容器运行时使用。但是容器可能被销毁,所以接下来要做的是让容器使用保存在主机中的数据。为了实现这一功能,我们将使用Docker中的绑定挂载功能。当您使用绑定挂载时,主机上的文件或目录会被挂载到容器中。

1
mkdir ~/www
1
vim ~/www/index.html

复制粘贴如下示例内容

1
2
3
4
5
6
7
8
<html>
<head>
<title>Nginx Docker</title>
</head>
<body>
<h1>My static page.</h1>
</body>
</html>

接下来,我们将运行Nginx Docker容器,并将容器 /usr/share/nginx/html 的目录映射到保存index.html文件的主机www目录上。

1
docker run -d -p 80:80 -v ~/www:/usr/share/nginx/html/ --name my-nginx-server nginx

Nginx 容器默认使用的文件目录是 /usr/share/nginx/html/,使用 -v 参数绑定 /usr/share/nginx/html/ 目录到主机的 ~/www 目录。

Docker使用冒号符号(:)将主机路径与容器路径分开。记住,主机路径总是第一位的。

如何通过 PHP-FPM 配置 Nginx 与 PHP 协同工作

1.安装 Nginx

Ubuntu / Debian

1
sudo apt install nginx

CentOS / AlmaLinux / Rocky Linux

Extra Packages for Enterprise Linux (EPEL)

1
sudo yum install epel-release
1
sudo yum install nginx

Fedora

1
sudo dnf install nginx

2.安装 PHP-FPM

Ubuntu / Debian

1
sudo apt install php-fpm

CentOS

CentOS 7

1
2
3
sudo yum install http://rpms.remirepo.net/enterprise/remi-release-7.rpm
sudo yum-config-manager --enable remi-php74
sudo yum install php php-fpm

CentOS 8

1
2
3
sudo yum install http://rpms.remirepo.net/enterprise/remi-release-8.rpm
sudo yum-config-manager --enable remi-php74
sudo yum install php php-fpm

3. 配置 Nginx 通过 PH-FPM 执行 PHP

创建 nginx 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sudo vim /etc/nginx/sites-available/example.com
````

编辑内容:

```shell
server {
listen 80;
root /var/www/html;
index index.php index.html index.htm;
server_name example.com;

location / {
try_files $uri $uri/ =404;
}

location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
}
}

保存后,创建软连接到 /etc/nginx/sites-enabled:

1
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com 

确保 fastcgi_pass 定义的 PHP socket 文件名是正确的,例子中是 php7.4-fpm.sock,

1
ls -l /var/run/php/

类似如下的内容:

1
2
3
4
total 4
-rw-r--r-- 1 root root 5 Dec 1 17:43 php7.4-fpm.pid
srw-rw---- 1 www-data www-data 0 Dec 1 17:43 php7.4-fpm.sock
lrwxrwxrwx 1 root root 30 Dec 1 17:43 php-fpm.sock -> /etc/alternatives/php-fpm.sock

如果不一致,按自己实际修改即可。

重启 Nginx 服务:

1
sudo systemctl restart nginx.service

4. 测试配置

上述设置中,站点文件根目录 /var/www/html, 创建测试文件:

1
echo "<?php phpinfo(); ?>" | sudo tee /var/www/html/info.php

测试访问 http://ip/info.php.

参考:

https://linuxiac.com/how-to-configure-nginx-to-work-with-php-via-php-fpm/

https://linuxiac.com/install-and-configure-nginx/