O projeto deste guia é pequeno, mas revela uma diferença importante: vamos construir um agente que responde perguntas sobre dependências e políticas percorrendo um grafo, em vez de apenas procurar textos semanticamente parecidos. No fim, ele recebe uma pergunta como “qual componente posso alterar sem quebrar a política de segurança?” e devolve os caminhos encontrados, junto com uma resposta gerada por um modelo de linguagem.
A busca vetorial pode encontrar trechos textuais semanticamente relevantes. O grafo faz outra coisa: preserva quem depende de quem, qual regra vale para qual área e quais relações precisam ser atravessadas para chegar à resposta.
O problema começa quando “parecido” não basta
Um embedding transforma um texto em uma lista de números. Embeddings são projetados para posicionar textos semanticamente relacionados em regiões próximas do espaço vetorial. Isso pode ajudar a encontrar uma explicação sobre cache quando a pergunta fala em “melhorar o tempo de resposta”.
Mas suponha que a documentação diga:
checkout depende de pagamentos
pagamentos depende de antifraude
antifraude exige criptografia em repouso
A pergunta “qual serviço está sujeito à política de criptografia em repouso?” não pede apenas um parágrafo parecido. Ela pede uma travessia: começar na política, encontrar o componente exigido, seguir até a dependência e descobrir os serviços afetados.
Essa distinção já está bem estabelecida na prática de sistemas de recuperação: vetores respondem “o que se parece com isto?”, enquanto grafos respondem “como isto se conecta àquilo?”. O projeto abaixo pretende transformar essa ideia em um exemplo executável.
Primeiro, faça o grafo andar
Crie uma pasta e salve o arquivo graph-agent.mjs. O primeiro passo não chama nenhum modelo. Isso é deliberado. Antes de adicionar geração de texto, precisamos testar se a estrutura de relações está correta.
O grafo será um objeto simples, com nós e arestas. Essa representação não pretende substituir um banco de grafos. Ela serve para entender o algoritmo e validar o fluxo mínimo sem adicionar dependências.
const graph = {
checkout: {
type: "service",
depends_on: ["pagamentos"]
},
pagamentos: {
type: "service",
depends_on: ["antifraude"]
},
antifraude: {
type: "service",
depends_on: [],
requires: ["criptografia-em-repouso"]
},
"criptografia-em-repouso": {
type: "policy",
depends_on: []
}
};
function pathsToPolicies(start, graph) {
const results = [];
const queue = [[start, [start]]];
const visited = new Set();
while (queue.length > 0) {
const [current, path] = queue.shift();
const key = path.join("->");
if (visited.has(key)) continue;
visited.add(key);
const node = graph[current];
if (!node) continue;
for (const policy of node.requires ?? []) {
results.push([...path, policy]);
}
for (const next of node.depends_on ?? []) {
queue.push([next, [...path, next]]);
}
}
return results;
}
console.log(pathsToPolicies("checkout", graph));
Execute no ambiente de execução JavaScript:
node graph-agent.mjs
A saída esperada, se o código for executado corretamente, é um caminho que liga os nós checkout, pagamentos, antifraude e criptografia-em-repouso. O ponto essencial está na fila. Cada relação vira um próximo passo explícito. Se houver ciclos, a estrutura visited impede que a navegação fique presa.
Agora acrescente o texto que explica cada nó
O grafo sabe que existe uma relação, mas não sabe explicar seu significado. Para isso, mantenha um documento curto associado a cada nó.
const documents = {
checkout: "O checkout coordena a finalização da compra e depende do serviço de pagamentos.",
pagamentos: "O serviço de pagamentos depende do antifraude para avaliar transações.",
antifraude: "O antifraude analisa risco e exige criptografia em repouso para os dados armazenados.",
"criptografia-em-repouso": "A política exige que dados armazenados permaneçam criptografados."
};
A busca vetorial entraria aqui em um sistema maior: você geraria um embedding para a pergunta, compararia esse vetor com os embeddings dos documentos e selecionaria os trechos mais próximos. A API de embeddings da HiNow devolve vetores de dimensão definida pelo serviço. A similaridade por cosseno pode ser calculada no próprio código, sem uma biblioteca adicional.
function dotProduct(a, b) {
return a.reduce((sum, value, i) => sum + value * b[i], 0);
}
function norm(vector) {
return Math.sqrt(dotProduct(vector, vector));
}
function cosine(a, b) {
return dotProduct(a, b) / (norm(a) * norm(b));
}
Não coloque o texto inteiro no grafo como se ele fosse uma relação. O documento responde “o que este nó significa?”. A aresta responde “qual caminho liga este nó a outro?”. Misturar as duas coisas dificulta depurar por que uma resposta apareceu.
Faça o modelo receber um contexto verificável
Depois de encontrar os caminhos, transforme-os em contexto. O modelo não deve inventar a estrutura. Ele deve redigir uma resposta a partir dela.
Adicione ao arquivo:
async function answer(question, paths, documents) {
const context = paths.map((path) => {
const explanations = path
.map((node) => `${node}: ${documents[node] ?? "sem descrição"}`)
.join("\n");
return `Caminho: ${path.join(" -> ")}\n${explanations}`;
}).join("\n\n");
const response = await fetch("https://api.hinow.ai/v1/chat/completions", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.HINOW_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "hinow/hinova",
messages: [{
role: "user",
content: [
"Responda usando somente os caminhos e documentos fornecidos.",
"Explique a cadeia de dependências e cite a política encontrada.",
`Pergunta: ${question}`,
`Contexto:\n${context}`
].join("\n\n")
}]
})
});
if (!response.ok) throw new Error(await response.text());
const data = await response.json();
return data.choices[0].message.content;
}
O endpoint e o identificador do modelo são marcadores porque precisam ser substituídos pelos valores confirmados na documentação da API usada no seu ambiente. A chave fica fora do código:
export HINOW_API_KEY="sua-chave"
node graph-agent.mjs
Onde entram os embeddings de verdade
A versão mínima navega o grafo a partir de um nó conhecido. Um agente real precisa descobrir esse nó a partir da pergunta. O fluxo fica assim:
- Gere um embedding da pergunta.
- Compare com os embeddings dos nós e documentos.
- Escolha as entidades mais próximas.
- Navegue suas relações no grafo.
- Recupere os documentos dos caminhos.
- Envie caminhos e textos ao modelo para redigir a resposta.
A busca vetorial faz a descoberta inicial. O grafo controla a lógica posterior. Esse desenho híbrido evita exigir que o embedding represente, sozinho, uma cadeia inteira de dependências.
Para uma base com poucos nós, calcular todos os cossenos no processo é suficiente. Em bases maiores, índices de vizinhos aproximados podem reduzir o tempo de busca, com possível perda de precisão. Uma consulta exige muitas operações de comparação, por isso a escolha do índice passa a importar quando o volume cresce.
Como validar sem confiar na fluência
Teste três perguntas, não apenas uma:
Qual política alcança o checkout por suas dependências?
O antifraude depende de pagamentos?
Quais caminhos saem de um nó inexistente?
A primeira deve encontrar um caminho até a política. A segunda deve distinguir a direção da relação, porque “pagamentos depende de antifraude” não significa automaticamente o inverso. A terceira deve retornar uma lista vazia, sem erro silencioso.
Meça também o que o modelo não pode corrigir. Se o grafo estiver errado, uma resposta bem escrita continuará errada. Esse é o limite central do agente: geração aumenta a clareza, mas não conserta uma relação ausente.
Se você fizer uma chamada para localizar entidades e outra para redigir a resposta, pode usar tempos hipotéticos apenas para dimensionar o fluxo. Por exemplo, suponha, apenas como ilustração, 300 ms para cada chamada e 2 s para cada rodada de inferência. Nesse cenário hipotético, a soma seria cerca de 2,6 s, sem contar a navegação local. Isso não é uma estimativa aplicável a todo ambiente. O tempo real depende do serviço e do tamanho do contexto. Se cada documento recuperado for grande, custo e latência sobem juntos.
Uma possível evolução é trocar o objeto em memória por uma base de grafos, guardar propriedades nas arestas e registrar os caminhos usados em cada resposta. Preserve essa trilha. Quando alguém perguntar “por que o agente concluiu isso?”, a melhor resposta não será outra frase do modelo. Será o caminho que ele percorreu.




